# Reporting the Current Screen

URL: https://www.digia.tech/docs/developer/reference/screen-tracking

**`Digia.setCurrentScreen`** tells Digia Engage which screen the user is on. This page is the implementation guide: where the call goes in each stack, how to wire it to your navigation library, and how to choose names that actually match your campaigns.

For *what* screen targeting does and how a campaign owner uses it, seeScreen Targeting .

info
`setCurrentScreen` is a Digia Engage **core SDK** capability. It behaves the same across React Native, Android, iOS, and Flutter, and across every integration — whether a CEP (CleverTap, MoEngage, WebEngage) drives delivery or Digiaruns its own campaigns .

## What the screen name drives

It is not only for targeting. One call feeds five things:

| Consumer | What the screen name does | **Campaign eligibility** | A screen-targeted campaign renders only while the current screen is one of its targets. | **Your CEP's screen view** | Forwarded to CleverTap `recordScreenView` , MoEngage `trackScreen` , or WebEngage `screen` . **This replaces your own CEP "record screen" call** — you do not need both. | **Analytics** | Attached as `screen_name` to every impression Digia records. | **Spotlight targeting** | Anchorless spotlight steps match on the screen name. | **Component capture** (debug) | Anchors are **skipped entirely** while no screen is set, so capture comes back empty. 

On Android and iOS the screen name also feeds screen-based frequency capping.

## Automatic or manual?

There are two ways to report screens, and they are not equivalent.

| | **Automatic** (wire a navigator once) | **Manual** (call per screen) | Effort | One-time setup | One line per screen | Name you get | Whatever your router calls the route | Whatever you choose | Covers tab switches | **No** | Yes | Covers unnamed routes | No | Yes | Available on | Flutter, iOS | All stacks 

**Recommendation: manual, with a central constants file.** It is one line per screen, it gives you names that match your dashboard, and it handles the cases a router cannot see. Reach for the automatic path when your app is large and already uses named routes that are exactly the strings your campaign owners type.

What ships today:

| Stack | Automatic helper | **Flutter** | `DigiaNavigatorObserver` | **iOS** | `DigiaNavigationScreenObserver` (opt-in `UINavigationControllerDelegate` ) | **Android** | None — call manually | **React Native** | None — call manually 

## Where the manual call goes

The subtle part is picking a lifecycle hook that fires on **every arrival** — not just the first mount. A screen the user navigates back to, or a tab they re-select, is still an arrival.

- React Native - Flutter - Android - iOS (Swift)
Use **`useFocusEffect`** , not `useEffect` . A tab screen stays mounted when you navigate away, so `useEffect` fires once and never again:
```tsx
import { useFocusEffect } from '@react-navigation/native'; // or from 'expo-router'
import { useCallback } from 'react';
import { Digia } from '@digia-engage/core';

export function useScreenTracking(screenName: string) {
  useFocusEffect(
    useCallback(() => {
      Digia.setCurrentScreen(screenName);
    }, [screenName]),
  );
}
```

Then one line per screen:
```tsx
export function HomeScreen() {
  useScreenTracking(ScreenNames.home);
  return /* … */;
}
```

**Not using hooks?**`Digia` is a plain singleton, so `Digia.setCurrentScreen(...)` works from `componentDidMount` , a navigation listener, a saga, or any non-React module. The hook is convenience, never the only path.

`initState` fires once per mount and **not** when the user navigates back — the screen underneath stays mounted the whole time. So `initState` alone silently under-reports. Either let `DigiaNavigatorObserver` handle back-navigation (it reports the revealed route on pop), or report explicitly at each entry point:
```dart
class CheckoutScreen extends StatefulWidget { /* … */ }

class _CheckoutScreenState extends State<CheckoutScreen> {
  @override
  void initState() {
    super.initState();
    Digia.setCurrentScreen(ScreenIds.checkout);
  }
}
```

For tabs and other cases the observer cannot see, see section 5 .

Use `onResume()` , not `onCreate()` , so the call repeats when the user returns:
```kotlin
import com.digia.engage.digiaScreen

class CheckoutActivity : AppCompatActivity() {
    override fun onResume() {
        super.onResume()
        digiaScreen(ScreenIds.CHECKOUT)
    }
}
```

`digiaScreen(...)` is available on both `Activity` and `Fragment` . In Compose, use the `DigiaScreen` composable:
```kotlin
import com.digia.engage.DigiaScreen

@Composable
fun CheckoutScreen() {
    DigiaScreen(ScreenIds.CHECKOUT)
    // … your screen
}
```

warning
`DigiaScreen` reports when the composable enters composition or its name changes. Inside a `HorizontalPager` that keeps neighbouring pages composed, swiping between pages will **not** re-report — drive it from the pager's selected index instead.

Use `viewDidAppear` , not `viewDidLoad` :
```swift
import DigiaEngage

final class CheckoutViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        digiaScreen(ScreenIds.checkout)
    }
}
```

In SwiftUI, call from `.onAppear`**at the screen's root view only** :
```swift
struct CheckoutScreen: View {
    var body: some View {
        ScrollView { /* … */ }
            .onAppear { Digia.setCurrentScreen(ScreenIds.checkout) }
    }
}
```

warning
Inside a `List` or `LazyVStack` , `.onAppear` fires per row as cells scroll into view. Attach it to the screen's root view, never to a row.

## Set the screen before your triggers fire

This is the most common cause of a campaign that "just doesn't appear".

- **A campaign dropped for the wrong screen is not retried.** If a campaign triggers while the current screen doesn't match, it is dropped there and then. It does **not** re-appear when the user later arrives on the targeted screen — your CEP has to trigger it again.
- **The screen is never remembered across launches.** After every cold start it is unset, and while it is unset *every* screen-targeted campaign is dropped.
So report the first screen as early as you can — right after `Digia.initialize(...)` , or from your first screen's arrival hook — so a launch-triggered campaign has a screen to match against.

### Re-report when the app returns to the foreground

When the user backgrounds your app and comes back, they are on the same screen but your CEP may need the in-app re-requested. The SDK does not do this for you. Re-report the last screen on foreground:

```ts
import { AppState } from 'react-native';

let currentScreen = ScreenNames.home;

AppState.addEventListener('change', (state) => {
  if (state === 'active') Digia.setCurrentScreen(currentScreen);
});
```

The same pattern applies on other stacks ( `scenePhase` on iOS, `ON_START` on Android, `AppLifecycleListener` on Flutter). Repeating the same screen name is safe.

warning
**React Native:** a screen reported *before*`Digia.initialize(...)` finishes is not applied to screen-triggered campaigns. Report your first screen after initialization resolves.

## Flutter with your router

- GoRouter - Plain Navigator - AutoRoute / other - Tab shells
`DigiaNavigatorObserver` works with GoRouter — pass it in `observers` :
```dart
final router = GoRouter(
  observers: [DigiaNavigatorObserver()],
  routes: [
    GoRoute(name: 'checkout', path: '/checkout', builder: (_, __) => const CheckoutScreen()),
  ],
);
```

What name lands depends on how you declared the route:
- **Named route** ( `GoRoute(name: 'checkout', …)` ) → reports `checkout` . Naming your routes gives you clean dashboard names for free — this is the recommended setup.
- **Unnamed route** → reports the **path pattern** , for example `/product/:id` , not the resolved `/product/42` . That is stable across products (good for targeting) but is not a string a campaign owner would naturally type, so align the dashboard to it or name the route.
warning
Two things that silently break this:
- A custom `pageBuilder` returning `CustomTransitionPage` must pass `name:` itself, otherwise that route reports nothing.
- `GoRouter` , `ShellRoute` , and `StatefulShellBranch` each take their own `observers` list, and a single observer instance can only be attached to one `Navigator` . Nested navigators need their own `DigiaNavigatorObserver()` instance each.

Add the observer to `MaterialApp` :
```dart
MaterialApp(
  navigatorObservers: [DigiaNavigatorObserver()],
  builder: (context, child) => DigiaHost(child: child!),
);
```

It reports `RouteSettings.name` on push, replace, and back-navigation. Routes without a name report nothing — including `showDialog` and `showModalBottomSheet` , which is intentional: a dialog is not a screen change, and the screen stays whatever it was underneath. If your routes are unnamed, or you want dashboard-friendly names instead of `/checkout` , call `Digia.setCurrentScreen` manually instead.

Any router that renders through a `Navigator` accepts a `NavigatorObserver` , so `DigiaNavigatorObserver` can be attached — but the name it sees is whatever that router puts in `RouteSettings.name` , which varies. If the reported names don't match your dashboard, report manually from each screen instead ( section 3 ). That is always available and always exact.

**A tab switch is a screen change with no route push, so no observer can see it.** If your app has a bottom navigation bar over an `IndexedStack` , you must report tab changes yourself:
```dart
class _MainShellState extends State<MainShell> {
  static const _tabScreenIds = [
    ScreenIds.home,
    ScreenIds.brands,
    ScreenIds.account,
    ScreenIds.cart,
  ];

  @override
  void initState() {
    super.initState();
    Digia.setCurrentScreen(_tabScreenIds[currentIndex]);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(index: currentIndex, children: _screens),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        onTap: (index) {
          Digia.setCurrentScreen(_tabScreenIds[index]);
          // … your own tab handling
        },
      ),
    );
  }
}
```

Report in **both**`initState` (for the tab the app opens on) and `onTap` .

## React Native with your navigator

- React Navigation - Expo Router - react-native-navigation - No navigation library
Per-screen with `useFocusEffect` is the recommended setup — see section 3 . If you cannot touch every screen, a container-level listener is a fallback:
```tsx
<NavigationContainer
  ref={navigationRef}
  onStateChange={() => {
    const routeName = navigationRef.getCurrentRoute()?.name;
    if (routeName) Digia.setCurrentScreen(routeName);
  }}
>
```

warning
This fires on **any** navigation state change, including route parameter updates, so the same screen is reported repeatedly. It also reports the route's `name` prop, which is often not the name your campaign owner would use. Prefer the per-screen hook.

Identical to React Navigation — import `useFocusEffect` from `expo-router` instead:
```tsx
import { useFocusEffect } from 'expo-router';
```

Everything else, including the hook in section 3 , is unchanged.

Wix's `react-native-navigation` has no React focus hook. Use its own lifecycle listener, registered once at startup:
```ts
import { Navigation } from 'react-native-navigation';
import { Digia } from '@digia-engage/core';

Navigation.events().registerComponentDidAppearListener(({ componentName }) => {
  Digia.setCurrentScreen(SCREEN_NAMES[componentName] ?? componentName);
});
```

Map component names to your dashboard names rather than reporting the raw component name.

Plenty of React Native apps swap screens with state rather than a router. Report the screen wherever you make that switch:
```tsx
function App() {
  const [screen, setScreen] = useState(ScreenNames.home);

  useEffect(() => {
    Digia.setCurrentScreen(screen);
  }, [screen]);

  return screen === ScreenNames.home ? <Home onNext={setScreen} /> : <Details />;
}
```

Because there is no navigator, this single effect is complete — it fires on every switch, forward or back.

## Choosing screen names

The name you report is matched **exactly and case-sensitively** against the campaign's target screens. `Checkout` , `checkout` , and `/checkout` are three different screens, and a mismatch fails silently.

**Use semantic names from a shared constants file — not route paths.** Every automatic source hands you a string a campaign owner would not naturally type:

| Source | What you get | GoRouter (unnamed route) | `/product/:id` | Navigation-Compose | `product/{id}` | iOS navigation observer | `HomeViewController` | React Navigation | whatever the route's `name` prop is 

One constants file per app keeps developers and campaign owners on the same vocabulary:

- React Native - Flutter

```ts
export const ScreenNames = {
  home: 'home',
  cart: 'cart',
  productDetail: 'product_detail',
} as const;
```

```dart
class ScreenIds {
  ScreenIds._();

  static const home = 'home';
  static const cart = 'cart';
  static const productDetail = 'product_detail';
}
```

Conventions that avoid trouble: lowercase `snake_case` , no leading slash, and **never a trailing space** — a stray space in a dashboard screen name matches on some platforms and not others.

warning
Agree the vocabulary with whoever builds campaigns, and use those exact strings on both sides. A mismatch produces no error — the campaign is simply dropped, with only a debug log.

## Verify it's working

With verbose logging enabled, the SDK logs every screen you report — something like `Current screen set: checkout` . If you never see it, your call isn't running; if you see it with the wrong value, your naming is the problem.

When a campaign is dropped for the screen, the SDK logs both sides of the comparison — the current screen (shown as `<unset>` if you never reported one) and the campaign's target screens. That log line tells you immediately whether the fix is to report a screen, rename it, or change the campaign.

For an end-to-end check, useLive Testing to fire a screen-targeted campaign at your device, andSDK Debug Settings to inspect SDK state on-device.

## Use it correctly

- **Report every screen the user lands on** , as early as possible and on every arrival — including the first screen after launch.
- **Use a lifecycle hook that repeats** : `useFocusEffect` , `onResume` , `viewDidAppear` — not `useEffect` , `onCreate` , or `viewDidLoad` .
- **Report tab switches yourself.** No navigator observer can see them.
- **Keep names identical to the dashboard** (exact, case-sensitive). This is the single most common reason a screen-targeted campaign doesn't appear.
- **Set the screen before campaigns trigger.** A campaign dropped for the wrong screen is never retried.
- **Delete your own CEP screen-view call.**`setCurrentScreen` already forwards it.

## Related pages

- Screen Targeting — what screen targeting does and which campaigns honor it
- Core Concepts
- User Identity
- Digia as Your CEP
