Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by Jigar Panchal on Unsplash

 

Koin has straightforward APIs to manage custom scopes and Compose Navigation can easily express app flows as nested navigation graphs, but how can we connect these two worlds to automatically connect to a scope when the user enters a flow and close it when that flow is exited? Read along if this sparked your interest.

This article assumes that you are familiar with the following two topics; in case they are new to you, you can quickly get up to speed by visiting the linked resources:

Scope & Navigation Definition

For the solution described in this article we will make use of one convention in order to make a logical bridge between app flows and scopes and that is: the route of a nested nav graph is used as scope identifier.

We start by describing our nested nav graph and here please pay attention to the declared route, we will use it again in the next step when declaring the scope tied to this flow.

const val ROUTE_GRAPH_FLOW: NavGraphRoute = "route_graph_flow"

/**
 * Navigation graph of the custom flow.
 */
fun NavGraphBuilder.flowNavGraph(
    navController: NavController,
) {
    navigation(
        startDestination = routeScreen1.getRouteWithPlaceholders(),
        route = ROUTE_GRAPH_FLOW,
    ) {
        screen1(
            onNextClick = {
                navController.navigateToScreen2()
            }
        )
        screen2(
            onFinishFlowClick = {
                navController.navigateToStart()
            }
        )
    }
}

As you can see below, the definition of the scoped dependencies comes with no surprises. The important aspect to be noticed here is that the name of the scope qualifier is the route we have declared above, to fulfil our convention.

val scopeModule = module {

    /**
     * Dependencies of [ROUTE_GRAPH_FLOW] scope.
     */
    scope(named(ROUTE_GRAPH_FLOW)) {

        viewModel {
            Screen1ViewModel(
                flowRepository = get(),
            )
        }

        // add other viewModels

        scoped<FlowRepository> {
            FlowRepositoryImpl(
                authRepository = get()
            )
        }
        
        // add other repositories, use cases, DAOs        
    }
}

⚡ Quick Refresher: A dependency coming from a narrow scope can rely on a dependency coming from a larger one, but vice-versa is not possible. If we want to inject a scoped repository, the viewModel that makes use of it has to be also scoped. That’s why in the definition above the viewModel is declared alongside the repository and is not placed in the root scope.

Injecting Scoped Dependencies

The setup is ready, now we are moving to the injection of our scoped dependencies. We inject the dependency that is highest in our architectural layers and that is the viewModel.

For this purpose, koinViewModel is the API we are looking for. Its signature reveals two relevant aspects for our solution. Firstly, it takes a scope as parameter, so injecting a viewModel tied to route_graph_flow scope is a breeze and looks like this:

val scope = koinInstance.getOrCreateScope(
                 scopeId = "route_graph_flow",
                 qualifier = named("route_graph_flow")
            )
            
val viewModel: ScopedViewModel = koinViewModel(
     scope = scope
)

And secondly, the default value of the scope parameter is LocalKoinScope.current. This is brilliant because it allows us to implicitly pass a scope using the CompositionLocal mechanism.

The solution

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.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, , ,

Inject your Jetpack Compose Application with Koin

Koin (insert-koin.io) is the Kotlin dependency injection framework. The Android community knows Koin very well as they have been using it since 2017. What they appreciate the most is its versatility and ease of use.
Watch Video

Inject your Jetpack Compose Application with Koin

Arnaud Giuliani
Koin Project Lead

Inject your Jetpack Compose Application with Koin

Arnaud Giuliani
Koin Project Lead

Inject your Jetpack Compose Application with Koin

Arnaud Giuliani
Koin Project Lead

Jobs

  • 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.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
Menu