---
title: Customize autocaptured screens - Flutter
description: Customize screen names collected via autocapture in Flutter apps
lastUpdated: 05 January 2026
source_url:
  html: https://docs.contentsquare.com/en/csq-sdk-flutter/product-analytics/customize-autocaptured-screens/
  md: https://docs.contentsquare.com/en/csq-sdk-flutter/product-analytics/customize-autocaptured-screens/index.md
---

## Track screens automatically

Automatic screen view tracking can be done using the `CSQNavigatorObserver`. This will observe the navigation stack of the application and log screen events accordingly.

This observer can be attached to any `WidgetApp` (like `MaterialApp`, `CupertinoApp`) or to a `RouterConfig`

* MaterialApp

  ```dart
  import 'package:contentsquare/csq.dart';


  MaterialApp(
    navigatorObservers: [
      CSQNavigatorObserver(),
    ],
  );
  ```

* Navigator

  ```dart
  import 'package:contentsquare/csq.dart';


  Navigator(
    observers: [
      CSQNavigatorObserver(),
    ],
  );
  ```

* GoRouter

  ```dart
  import 'package:contentsquare/csq.dart';


  final _goRouter = GoRouter(
    observers: [
      CSQNavigatorObserver(),
    ],
    routes: [...],
  );


  MaterialApp.router(
    routerConfig: _goRouter,
  );
  ```

Tip

We recommend using [Routes ↗](https://api.flutter.dev/flutter/widgets/Route-class.html) with proper [names ↗](https://api.flutter.dev/flutter/widgets/RouteSettings/name.html) assigned to them. This will be used as a fallback value for the automatic screen name capturing.

The `CSQNavigatorObserver` can be configured with the following arguments:

### ExcludeRouteFromTracking

An optional callback function that determines whether a specific route should be excluded from tracking. The function takes a `Route` as input and returns a `bool`.

Use this function to selectively exclude certain routes from automatic tracking by returning true for those routes.

```dart
import 'package:contentsquare/csq.dart';


CSQNavigatorObserver(
  excludeRouteFromTracking: (route) {
    // Exclude a specific route name
    if (route.settings.name == '/myRoute') return true;


    // Exclude popup routes (menus, popups, dialogs)
    if (route is PopupRoute) return true;


    // Exclude dialog routes (AlertDialog, showDialog)
    // Many dialogs inherit from PopupRoute, but this is extra safety:
    if (route.runtimeType.toString().contains('DialogRoute')) return true;


    // Exclude full-screen dialogs
    if (route is PageRoute && route.fullscreenDialog == true) return true;


    return false;
  },
)
```

By default, all routes are tracked.

Note

`CSQNavigatorObserver` consider modal routes (like `showDialog`, `showModalBottomSheet`, etc.) and pop-ups (such as `showDialog`) as separate screens. If you don't want to track them as screens, you can use the `excludeRouteFromTracking` configuration option to exclude them.

### ScreenNameProvider

An optional callback function used to customize the screen name for a given route. The function takes a `Route` as input and returns a `String` representing the desired screen name.

Use this function to define custom screen names for specific routes.

```dart
import 'package:contentsquare/csq.dart';


CSQNavigatorObserver(
  screenNameProvider: (route) {
    final screenName = _formatScreenName(route.settings.name);
    return screenName;
  },
)
```

Tip

If you use dart code obfuscation in your release builds, we recommend to use the `screenNameProvider` to avoid obfuscated screen names being tracked.

## Troubleshooting

### AutoRoute package support

If you're using the [AutoRoute package ↗](https://pub.dev/packages/auto_route), refer to the [CSQNavigatorAutoRouteObserver](https://docs.contentsquare.com/en/csq-sdk-flutter/product-analytics/auto_route_support/) documentation for automatic screen tracking support, including tab navigation.

### Pageview widget

`PageView` widgets sometimes are used to build swipeable screens such as onboarding flows, multi-step forms, or carousels. However, because `PageView` navigation does not push new routes to the navigation stack, navigator observers are not triggered when pages change.

To ensure correct tracking, each page transition must be tracked manually using the `onPageChanged` callback.

For example, when using `PageView.builder`, you can track each page as an individual screen:

```dart
import 'package:contentsquare/csq.dart';


class PageViewTrackingExample extends StatelessWidget {
  const PageViewTrackingExample({super.key});


  final _controller = PageController();


  // Define meaningful screen names for each page
  final _pageNames = const <String>[
    'onboarding_step_1',
    'onboarding_step_2',
    'onboarding_step_3',
  ];


  @override
  Widget build(BuildContext context) {
    return PageView.builder(
      controller: _controller,
      itemCount: _pageNames.length,
      onPageChanged: (index) {
        // Track screen view manually for each page change
        CSQ().trackScreenview(screenName: _pageNames[index]);
      },
      itemBuilder: (_, index) {
        return OnboardingPage(step: index);
      },
    );
  }
}
```

### PageBuilder

If you use a `pageBuilder` in your routes, make sure to set the page `.name`.

Example using `GoRoute`:

```dart
import 'package:contentsquare/csq.dart';


GoRoute(
  path: 'checkout',
  name: 'Checkout Screen',
  pageBuilder: (context, state) => MaterialPage(


    name: 'Checkout Screen',
    key: ValueKey(state.pathParameters),
    fullscreenDialog: true,
    child: const CheckoutScreen(),
  ),
),
```

### TabBar

`TabBar` views are commonly tracked as individual screens. However, most `TabBar` implementations push the route to the navigation stack only during initialization, which can make it challenging to properly track navigation events for each tab change.

To track tab changes correctly, you need to manually call `trackScreenview` when the active tab changes. Here's an example using `TabController`:

```dart
import 'package:contentsquare/csq.dart';


class TabBarExample extends StatefulWidget {
  const TabBarExample({super.key});


  @override
  State<TabBarExample> createState() => _TabBarExampleState();
}


class _TabBarExampleState extends State<TabBarExample>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;


  // Define meaningful screen names for each tab
  final _tabNames = const <String>[
    'home_tab',
    'search_tab',
    'profile_tab',
  ];


  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabNames.length, vsync: this);


    // Track initial tab
    CSQ().trackScreenview(screenName: _tabNames[_tabController.index]);


    // Listen to tab changes
    _tabController.addListener(() {
      if (!_tabController.indexIsChanging) {
        CSQ().trackScreenview(screenName: _tabNames[_tabController.index]);
      }
    });
  }


  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(icon: Icon(Icons.home), text: 'Home'),
            Tab(icon: Icon(Icons.search), text: 'Search'),
            Tab(icon: Icon(Icons.person), text: 'Profile'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const [
          HomeTab(),
          SearchTab(),
          ProfileTab(),
        ],
      ),
    );
  }
}
```
