> For the complete documentation index, see [llms.txt](https://docs.digia.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.digia.tech/digia-engage/developer-guides/reference/screen-tracking.md).

# Reporting the Current Screen

Where to call Digia.setCurrentScreen in every stack — navigator observers, per-screen hooks, router-specific recipes, and how to pick screen names that match your campaigns.

**`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, see [Screen Targeting](/digia-engage/developer-guides/reference/screen-targeting.md).

{% hint style="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 Digia [runs its own campaigns](/digia-engage/developer-guides/integrations/digia-cep.md).
{% endhint %}

***

## 1. 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.

***

## 2. 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                                                      |

***

## 3. 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.

{% tabs %}
{% tab title="React Native" %}
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.
{% endtab %}

{% tab title="Flutter" %}
`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](#5-flutter-with-your-router).
{% endtab %}

{% tab title="Android" %}
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
}
```

{% hint style="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.
{% endhint %}
{% endtab %}

{% tab title="iOS (Swift)" %}
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) }
    }
}
```

{% hint style="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.
{% endhint %}
{% endtab %}
{% endtabs %}

***

## 4. 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.

{% hint style="warning" %}
**React Native:** a screen reported *before* `Digia.initialize(...)` finishes is not applied to screen-triggered campaigns. Report your first screen after initialization resolves.
{% endhint %}

***

## 5. Flutter with your router

{% tabs %}
{% tab title="GoRouter" %}
`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.

{% hint style="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.
  {% endhint %}
  {% endtab %}

{% tab title="Plain Navigator" %}
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.
{% endtab %}

{% tab title="AutoRoute / other" %}
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](#3-where-the-manual-call-goes)). That is always available and always exact.
{% endtab %}

{% tab title="Tab shells" %}
**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`.
{% endtab %}
{% endtabs %}

***

## 6. React Native with your navigator

{% tabs %}
{% tab title="React Navigation" %}
Per-screen with `useFocusEffect` is the recommended setup — see [section 3](#3-where-the-manual-call-goes).

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);
  }}
>
```

{% hint style="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.
{% endhint %}
{% endtab %}

{% tab title="Expo Router" %}
Identical to React Navigation — import `useFocusEffect` from `expo-router` instead:

```tsx
import { useFocusEffect } from 'expo-router';
```

Everything else, including the hook in [section 3](#3-where-the-manual-call-goes), is unchanged.
{% endtab %}

{% tab title="react-native-navigation" %}
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.
{% endtab %}

{% tab title="No navigation library" %}
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.
{% endtab %}
{% endtabs %}

***

## 7. 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:

{% tabs %}
{% tab title="React Native" %}

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

{% endtab %}

{% tab title="Flutter" %}

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

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

{% endtab %}
{% endtabs %}

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.

{% hint style="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.
{% endhint %}

***

## 8. 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, use [Live Testing](/digia-engage/user-guides/guides.md) to fire a screen-targeted campaign at your device, and [SDK Debug Settings](/digia-engage/developer-guides/reference/debug-settings-deeplink.md) to inspect SDK state on-device.

***

## 9. 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](/digia-engage/developer-guides/reference/screen-targeting.md) — what screen targeting does and which campaigns honor it
* [Core Concepts](/digia-engage/start-here/core-concepts.md)
* [User Identity](/digia-engage/developer-guides/reference/user-identity.md)
* [Digia as Your CEP](/digia-engage/developer-guides/integrations/digia-cep.md)
