Learning Android Development

Kotlin Koin Scope Illustrated

Defining the scope of your dependencies with Koin

Picture by annca on Pixarbay

If you are familiar with Dagger 2, you probably know that Scope an important part of Dependency Injection. It enables us to determine if we are getting the same dependent object or a new one.

What about Koin, the popular Kotlin way of getting your dependencies? The scope documentation for core and android Koin is a good start, but I spent some time digging out all the gist of it. Here I provide some illustration, and some gotcha to watch out.

Single and Factory

The simplest scope available in Koin is single and factory .

val koinModule =

module {

single(qualifier = named("singleName")) { Dependency() }

factory(qualifier = named("factoryName")) { Dependency() }

}

To illustrate this easily, I use the same Dependency object for both single and factory scope. The are illustrated below

Single — always provide the same copy of Dependency object for the requester Factory — always provide a new copy of Dependency object for the requester

To get the dependency , (as I qualified it with proper names), it is as below

val alwaysSameDependency = get<Dependency>(

qualifier = named("singleName")).toString() val alwaysNewDependency = get<Dependency>(

qualifier = named("factoryName")).toString()

Gotcha!

The single might sounds like it’s a Singleton. It is not really one, unless it is generated on the root module.

For the case of it is in a loaded module as loaded

loadKoinModules(koinModule)

upon the lifetime of the module is loaded, it will always generate the same copy. However if the module is unloaded as below

unloadKoinModules(koinModule)

and then reloaded later, a new copy of the single dependency will be created instead.

Qualified Scope

Both single and factory live throughout the lifetime of the koin module loaded. However if we want some dependencies that has a scope differ from the module lifetime, we could use Qualified Scope. There are 2 of them.

String Qualified Scope

Instead of dependent on the Module solely, we could create a Scope independently from the Module Lifetime.

val koinModule =

module {

scope(named("AScopeName")) {

scoped(qualifier = named("scopedName")) { Dependency() }

factory(qualifier = named("factoryName")) { Dependency() }

}

}

As we could see above, we have a scope with the name of AScopeName . To create the scope within the koinModule , just need to do the following.

val stringQualifiedScope = getKoin().createScope(

"ScopeNameID", named("AScopeName"))

The ScopeNameID is the ScopeID that one could use to identify the Scope from one to the other. It is used internally as the Key (ID) to look for this scope. It’s used is described later.

Here we have scoped and factory .

Scoped — always provide the same copy of Dependency object from the Scope for the requester Factory — always provide a new copy of Dependency object from the Scope for the requester

To get the dependencies, is as below

val alwaysSameDependency = stringQualifiedScope.get<Dependency>(

qualifier = named("scopedName")).toString() val alwaysNewDependency = stringQualifiedScope.get<Dependency>(

qualifier = named("factoryName")).toString()

To close away the scope (to end it life), one just need to call close as below

stringQualifiedScope.close()

Type Qualified Scope

Type qualified scope is same as String Qualifies Scope with the exception it is not tied to a String named made up scope, but a variable type.

In example below, we have a class type Container , where once we have the object, we could get scope from it.

val koinModule =

module {

factory { Container() } scope<Container> {

scoped(qualifier = named("scopedName")) { Dependency() }

factory(qualifier = named("factoryName")) { Dependency() }

}

}

Actually TypeQualifiedScope is a little misnomer. It is actually differs from Object to Object even if they are of the same type (i.e. two

So to create the scope, we first need to get the Container object first.

val container: Container = get()

val typeQualifiedScope = container.scope

To get the dependencies, is the same as stringQualifiedScope above.

val alwaysSameDependency = typeQualifiedScope.get<Dependency>(

qualifier = named("scopedName")).toString() val alwaysNewDependency = typeQualifiedScope.get<Dependency>(

qualifier = named("factoryName")).toString()

To close away the scope (to end it life), one just need to call close as below

typeQualifiedScope.close()

Gotcha!

When getting scope from object e.g. container.scope , it is actually container.getOrCreateScope() , so if one uses container.scope.close() , container.scope still works as it create a new one, making one confuse why after closing, it could still be used

For StringQualifiedScope, only after the close() is called on the scope, can one recreate a scope with the same ScopeID i.e. ScopeNameID , else it will crash. This doesn’t applies to TypeQualifiedScope though, as every time we get the scope from the object, it will provide the same scope instead of crashing.

is called on the scope, can one recreate a scope with the same ScopeID i.e. , else it will crash. This doesn’t applies to TypeQualifiedScope though, as every time we get the scope from the object, it will provide the same scope instead of crashing. Can we have single in the scope ? NO. Only scoped is allowed. single is error out during compile time. Essentially scoped and single is the same thing, but used in different place. I propose to them to use the same name as in

Other Features

Other than the normal scope, there are other feature provided which is handy for scope usage. They are as below

Link scope

In order to get some dependencies of another scope from one scope, we could link them together.

E.g. the below diagram showing we link scope_a with scope_b as well as scope_a link with scope_a (we could link just one of them, but I’m showing they could link bidirectionally just to demonstrate that’s possible)

scopeA.linkTo(scopeB)

scopeB.linkTo(scopeA)

With that, we could

val somethingInScopeB = scopeA.get<SomeDependencyInScopeB>()

val somethingInScopeA = scopeB.get<SomeDependencyInScopeA>()

Passing parameters

There are cases where we want some of our dependencies getting some specific parameter e.g. from which Scope it should gets it’s dependencies, or the qualified name.

From the diagram below, we could send the parameter A, B and C, which is then provided to the dependency generator function.

Below is an example, we have an Environment which is dependent on Dependency . So we have to tell Environment which scope should it get its Dependency from.

class Environment(val dependency: Dependency) val mainKoinModule =

module {

single(qualifier = named("single")) { Dependency() }

factory(qualifier = named("factory")) { Dependency() }



scope(named("AScopeName")) {

scoped(qualifier = named("scopedName")) { Dependency() }

factory(qualifier = named("factoryName")) { Dependency() }

}



scope<Container> {

scoped(qualifier = named("scopedContainer")) {

Dependency()

}

factory(qualifier = named("factoryContainer")) {

Dependency()

}

}



factory { (scopeId: ScopeID, name: String) ->

Environment(getScope(scopeId).get(qualifier = named(name)))

}

}

In order to do that, we could pass the parameter over, upon what we set above

(scopeId: ScopeID, name: String)

So the way to get it is

val scopeID = someScope.id // ScopeID is actually a String

val qualifiedName = "someQualifier val environment = get<Environment>(

parameters = { parametersOf(scopeID, qualifierName) })

Android Lifecycle Scope

This is specifically to Android. (Note, you’ll need to get the koin-android library instead of koin-core library).

You could define your module using your Activity as the Type.

val androidModule = module {



scope<MyActivity> {

scoped { Dependency() }

}

}

To get your dependencies, you’ll just need to use the lifecycleScope instead of normal scope as shown above.

class MyActivity : AppCompatActivity() {



// inject Dependency instance from current scope

val dependency : Dependency by lifecycleScope.inject()

What does this provides further?

Tracing down the code, it’s actually automatically close upon ON_DESTROY, hence you don’t need to close it. That’s it.