DEV Community

Arsenii Kharlanow
Arsenii Kharlanow

Posted on

Koin - scopes

Koin is a simple-to-use DI library, without code generation.
In this article, I want to show how to use one of the features of Koin - scopes.

I suppose you faced a situation when you need to share an object between multiple components with lifetimes shorter than the app's lifetime. For example, you need to share some in-memory storage between two fragments inside one screen (it is more relevant for single activity app), in this case, you can use singleton but the singleton lifetime is equal to the app lifetime, or use the activity as a holder for this component.

One solution to this problem is to use Koin scopes. Scopes are a powerful tool that can help you manage the lifecycle of your components and ensure that objects are only created and destroyed when they're needed.

However, using scopes correctly isn't always straightforward. In this article, I'll share my experience and offer some tips on how to use Koin scopes effectively. By the end of this article, you'll better understand how to use scopes to manage your components' lifecycles and share objects between them.

According to the Koin documentation, Koin has 3 types of scopes:

  • single - in the case of Android lifetime of the component is equal to the app's lifetime
  • factory - create a new object each time
  • scoped - create an object that is persistently tied to the associated scope lifetime - and this is what we need.

Scoped is a singleton, but we have the ability to clear the object when we need and it will be recreated in the next time call. Inside Koin scopes are stored in the Map, where key is the name of the scope and clear scope means to remove it from this map.

Create a component with our own scope:

module {
    scope(named("myScope")) {
        scoped { Component() }
    }
}
Enter fullscreen mode Exit fullscreen mode

then we can use our component as a dependency in another component:

module {
   factory {
      Component2(
         component1 = getScope("myScope").get(),
      )
   }
}
Enter fullscreen mode Exit fullscreen mode

And we need to create a scope in the Fragment or Activity:

val scope = getKoin().createScope("myScope", named("myScope"))
Enter fullscreen mode Exit fullscreen mode

and when the components in the scope need to be removed, we can close the scope (for example we can call this method inside onDetach in the fragment):

scope.close()
Enter fullscreen mode Exit fullscreen mode

Koin provides a few methods for creating scope which is linked to the lifecycle of the Android components:
createActivityScope()
createActivityRetainedScope()
createFragmentScope()

Based on these methods we can create our own methods that can be used to create custom scopes.

For example, this extension method can be used for creating custom scopes inside Fragments:

fun Fragment.getOrCreateScope(scopeId: String): Scope {
    val scope = getScopeOrNull() ?: createScope()
    val activityScope = activity?.getScopeOrNull()
    activityScope?.let { scope.linkTo(it) }
    val customScope = getKoin().getScopeOrNull(scopeId) ?: getKoin().createScope(scopeId, named(scopeId))
    customScope.linkTo(scope)
    lifecycle.addObserver(object : LifecycleEventObserver {

        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY && customScope.closed.not()) {
                customScope.close()
            }
        }
    })
    return customScope
}
Enter fullscreen mode Exit fullscreen mode

Here we get or create a parent scope, then try to get an activity scope, and finally, create our own scope.
Method linkTo(...) is used for linking scopes for resolving dependencies.
The most important part here is adding a lifecycle observer in order to destroy our scope when the fragment is destroyed.

The same method we can create for Acivity:

fun AppCompatActivity.getOrCreateScope(scopeId: String): Scope {
    val activityScope = getScopeOrNull() ?: createScope()
    val customScope = getKoin().getScopeOrNull(scopeId) ?: getKoin().createScope(scopeId, named(scopeId))
    customScope.linkTo(activityScope)

    lifecycle.addObserver(object : LifecycleEventObserver {

        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY && customScope.closed.not()) {
                customScope.close()
            }
        }
    })
    return customScope
}
Enter fullscreen mode Exit fullscreen mode

In conclusion:

  • scope is the same as a singleton with the ability to destroy it when you want. Inside Koin scopes are stored in the map
  • when you define the scope in the DI, you need to create this scope in the Activity/Fragment where we want to use components that are defined with scopes.
  • you need to close the scope when the components became unused in other cases the scope will never be destroyed and it will be the same result as with using a 'singleton'
  • you can use Koin predefined methods for creating lifecycle-depending scopes, or create scopes and manage them by yourself.

Top comments (0)