Place Scope Handling on Auto-Pilot with Koin & Compose Navigation | by Mihai Batista | Oct, 2024

We’re moving to the final part of writing the composable that is taking care of the scope management, we will call it AutoConnectKoinScope. Now let’s look at the details.

navController.addOnDestinationChangedListener() is the key of the solution, as it gives us the current destination (screen) and implicitly the nested nav graph (flow) that the destination is part of. Adding our convention in the equation enables us to know which scope should be used to resolve the dependencies of a particular screen.

🔓 Key API: addOnDestinationChangedListener gives us the current destination (screen) and implicitly the nested nav graph (flow) that the destination is part of.

For clarity I’ve broke the solution into three parts:

  • 1st — For each visited screen the appropriate scope is resolved and passed implicitly through CompositionLocal mechanism.
if (currentNavGraphRoute != null) {
val scopeForCurrentNavGraphRoute = koinInstance.getOrCreateScope(
scopeId = currentNavGraphRoute,
qualifier = named(currentNavGraphRoute)
)
scopeToInject = scopeForCurrentNavGraphRoute
} else {
scopeToInject = rootScope
}

.....

CompositionLocalProvider(
LocalKoinScope provides scopeToInject,
content = content
)

💡 Good to Know: We don’t need to link a custom scope to the root scope, by default dependencies coming from the root scope can be resolved in a custom scope.

  • 2nd — The previous scope is closed when we detect that the user has moved to a new flow.
val currentNavGraphRoute = destination.parent?.route
val previousNavGraphRoute = lastKnownNavGraphRoute

if (previousNavGraphRoute != null && currentNavGraphRoute != previousNavGraphRoute) {
val lastScope = koinInstance.getOrCreateScope(
scopeId = previousNavGraphRoute,
qualifier = named(previousNavGraphRoute)
)
lastScope.close()
}

  • 3rd — The correct scope is restored in case the parent activity is being recreated, for this purpose we store the last known nav graph route in a global variable (in order to be kept around while the process lives) and use it to initialise the mutable state that holds the scope to be injected.
var scopeToInject by remember {
val lastKnownNavGraphRoute = lastKnownNavGraphRoute
mutableStateOf(
value = if (lastKnownNavGraphRoute != null) {
koinInstance.getOrCreateScope(
scopeId = lastKnownNavGraphRoute,
qualifier = named(lastKnownNavGraphRoute)
)
} else {
rootScope
}
)
}

To keep things clean & tidy we make use of DisposableEffect to unregister the OnDestinationChangedListener when AutoConnectKoinScope composable leaves the composition.

🍬 Koin for Compose Goodies: To get a hold of the current Koin instance, Koin provides us with the getKoin() composable, and to get the current scope with LocalKoinScope.current .

To have visibility over the entire navigation and be able to manage the scope for any screen of the app, we need to place the AutoConnectKoinScope composable at the top of our Compose hierarchy, somewhere above the app’s NavHost.

And that’s it folks 🎉, with this last piece we’ve placed the scope management on auto-pilot and we can move our focus on other aspects of the app craftsmanship.

Leave a Reply