How to properly track screen view on Android with Jetpack Compose UI?
Asked Answered
B

5

15

I want to track screen views in a compose-only app, so no activities, fragments, only composable functions.

When and how I should call the screen tracking e.g. for firebase?

Because the body of the composable function is executed on every recomposition, side effects can be also called repeatedly, and none of these means a real "view", composable can be invisible or just measured.

Bolivia answered 6/10, 2021 at 10:4 Comment(0)
D
13

You can use LaunchedEffect or DisposableEffect with a constant key, for example Unit, to execute side effects exactly once a composable enters composition (and in the case of DisposableEffect also on exit), ignoring recompositions.

// I don't think PascalCase would suit this method, as it suggests an on-screen element
@SuppressLint("ComposableNaming") 
@Composable
fun trackScreenView(name: String) {
    DisposableEffect(Unit){
        Log.d("SCREENTRACKING", "screen enter : $name")
        onDispose { Log.d("SCREENTRACKING", "screen exit : $name") }
    }
}

@Composable
fun Screen1() {
    trackScreenView("Screen 1")
    // your screen content here
}

@Composable
fun Screen2() {
    trackScreenView("Screen 2")
    // your screen content here
}
Durware answered 6/10, 2021 at 11:13 Comment(2)
Thank you for the answer! Any side effect is good enough for simple cases, but atm misleading in more complex cases like Pager layouts, LazyColumn/LazyRow, ModalDrawer. E.g. drawer content enters the composition "in the background" while not visible by the user. So I can't use side effect for tracking its view.Bolivia
Sure, for certain cases you need to add a bit more logic. A generic way of detecting whether a particular composable is actually visible to the user is not that easy, and most likely has a noticable performance impact ( using the onGloballyPositioned modifier comes to mind). Feel free to add a code sample that does not work with my implementation and I might come up with a solution.Durware
K
5

If you want to send analytics events based on Lifecycle events you can use a LifecycleObserver. To listen for those events in Compose, use a DisposableEffect to register and unregister the observer when needed.

@Composable
fun TrackedScreen(
    name: String,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
) {
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                Firebase.analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
                    param(FirebaseAnalytics.Param.SCREEN_NAME, name)
                }
            }
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

@Composable
fun HomeScreen() {
    TrackedScreen("Home")
   
    /* Home screen content */
}
Kristiankristiansand answered 16/8, 2022 at 11:38 Comment(1)
When using Jetpack Compose within an Activity, the LocalLifecycleOwner.current value will always refer to the lifecycle owner of the hosting Activity. In that case, if all the composables using TrackedScreen are executed within the same Activity, you can safely omit the lifecycleOwner parameter in the TrackedScreen composable and use Unit as the key parameter in the DisposableEffect.Aubine
W
1

If you use NavHostController for navigating between "top level" composables, you can use its .addOnDestinationChangedListener callback. Doing so will result in your analytics code to be in a central place somewhere in your app.

Weismannism answered 1/9, 2023 at 14:28 Comment(0)
B
1

While you could use a side effect on every screen level composable to track an event, you could also attach a listener to your navController to automate tracking of screen views with addOnDestinationChangedListener. This will fire an event every time the navController navigates to a new destination. For example:

 navController.addOnDestinationChangedListener { _, destination, _ ->
            val params = Bundle()
            params.putString(FirebaseAnalytics.Param.SCREEN_NAME, destination.route)
            firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, params)
        }
Benkley answered 4/10, 2023 at 12:39 Comment(0)
W
0

@Adrian K DisposableEffect, as in your answer will work pretty well even if you use Lazy Rows in a Lazy Column.

DisposableEffect(Unit){
    Log.d("SCREENTRACKING", "screen enter : $name")
    onDispose { Log.d("SCREENTRACKING", "screen exit : $name")
}

Only the on screen items have DisposableEffect called initially, and the scrolling down or side ways in the list the new items are called. The one problem that needs improvement for fine grained impression data is that scrolling back over the same items that have already been rendered before do not necessarily call DisposableEffect again.

(would have left a comment but need more reputation)

Whosoever answered 13/1, 2022 at 0:31 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.