So, i’m looking to put some logging into a NavHost...
# compose
m
So, i’m looking to put some logging into a NavHostController. I started with a state object that has an empty route string, and then a destination listener that updated the state object’s route string. The problem then is that i want to listen to the state object and do some logging as it changes. I tried a SideEffect, but for whatever reason, it’s only getting called a single time with the initial version of the state. When i create the nav graph i would have assumed that it would update the state (which it does), but it never triggers the side effect for that first update (subsequent updates work fine).
🧵 1
Copy code
data class PageViewState(
    val startTime: Long = System.currentTimeMillis(),
    val route: String = ""
)

@Composable
fun rememberMutablePageViewState(initialRoute: String = "") = remember {
    mutableStateOf(PageViewState(route = initialRoute))
}

fun MutableState<PageViewState>.attachToNavController(navController: NavHostController) {
    navController.addOnDestinationChangedListener(PageViewStateDestinationListener(this))
}

fun PageViewStateDestinationListener(pageViewState: MutableState<PageViewState>) =
    NavController.OnDestinationChangedListener { _, destination, _ ->
        pageViewState.value = pageViewState.value.copy(route = destination.route ?: "")
    }

@Composable
fun PageViewStateSideEffect(pageViewState: PageViewState) {
    SideEffect {
        Timber.tag("PageEnter").d(pageViewState.toString())
    }
}
As i said the issue is when the first screen renders, i see only the timber log with an empty route.
I suspect it’s because it’s a single composition and so side effect will not trigger twice on a single composition. Maybe i’m better off just handling the logging directly in the destination listener?
Update: So moving the call to PageViewStateSideEffect to after the declaration of the NavHost solves the issue. Again i suspect it’s because it’s only a single composition happening. Is there some other recommended way to handle something like this other than SideEffect?
Given the fact that i have to handle process backgrounding and foregrounding, i’m not even convinced i want to use remembered state anyway, and might just go with a standard viewmodel
i
Any kind of listener you attach should be using a `DisposableEffect`: https://developer.android.com/jetpack/compose/side-effects#disposableeffect
SideEffect
runs every single composition, which is most certainly not what you want for event logging
What you actually want for event tracking should probably be based on
currentBackStackEntryFlow
, not any listener at all:
Copy code
val navController = rememberNavController()
LaunchedEffect(navController) {
  navController.currentBackStackEntryFlow.mapNotNull { backStackEntry ->
    backStackEntry.destination.route
  }.distinctUntilChanged().map { destinationRoute ->
    PageViewState(route = destinationRoute)
  }.collect { pageViewState ->
    Timber.tag("PageEnter").d(pageViewState.toString())
  }
}
NavHost(...) {
  ...
}
👍 1
m
So i’m actually hoisting the action that occurs in the collect block back up the chain because there is other state involved in the logging. We have attributes that can be set in certain circumstances (which are also done with lambda functions), plus when we change routes, we have to log a page exit as well with a duration.
@Ian Lake can you explain to me the benefit of using LaunchedEffect vs a straight destination listener? the code above vs a simple
Copy code
navController.addOnDestinationChangedListener { _, destination, _ ->
  ...
}
either one is doing what i want, i just want to understand the reasoning why LaunchedEffect is prefered.
Is it just because you’re using a flow, so the collection scope can tie itself to the composition lifecycle?
i
Again, you can't call
addOnDestinationChangedListener
as part of composition
It would need to be in its own effect block, such as a
DisposableEffect
that removes the listener when the composition is removed
And there's no reason to go through the adding/removing of a listener when
collect
automatically cleans itself up
And because you want when the route changes, you really do want operators like
distinctUntilChanged()
, which you get for free with all of the many flow operations
m
Thanks for the explanation @Ian Lake. Makes sense to me.