Arnaud Giuliani <arnaud@insert-koin.io> - and on twitter: @arnogiu :revnumber: 2.0.0-GA4 :example-caption!: :imagesdir: images :sourcedir: ../kotlin

Covers Koin features for Android.

1. About koin-android

The koin-android project is dedicated to provide Koin powers to Android world.

For general Koin concepts, check the koin-core manual.

1.1. Gradle setup

Add the koin-android dependency to your Gradle project:

// Add Jcenter to your repositories if needed
repositories {
    jcenter()
}
dependencies {
    // Koin for Android
    implementation 'org.koin:koin-android:{revnumber}'
}

2. Start Koin with Android

The koin-android project is dedicated to provide Koin powers to Android world.

2.1. startKoin() from your Application

From your Application class you can use the startKoin function and inject the Android context with androidContext as follow:

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            // Koin Android logger
            androidLogger()
            //inject Android context
            androidContext(this@MainApplication)
            // use modules
            modules(myAppModules)
        }

    }
}

2.2. Starting Koin with Android context from elsewhere?

If you need to start Koin from another Android class, you can use the startKoin function and provide your Android Context instance with just like:

startKoin {
    //inject Android context
    androidContext(/* your androic context */)
    // use modules
    modules(myAppModules)
}

2.3. Koin Logging

Within your KoinApplication instance, we have an extension androidLogger which use the AndroidLogger()= This logger is an Android implementation of the Koin logger.

Up to you to change this logger if it doesn’t suits to your needs.

Shut off Koin Logger
class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            //inject Android context
            androidContext(/* your androic context */)
            // use Android logger - Level.INFO by default
            androidLogger()
            // use modules
            modules(myAppModules)
        }
    }
}

2.4. Properties

You can use Koin properties in the assets/koin.properties file, to store keys/values:

Use Koin extra properties
// Shut off Koin Logger
class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            //inject Android context
            androidContext(/* your androic context */)
            // use Android logger - Level.INFO by default
            androidLogger()
            // use properties from assets/koin.properties
            androidFileProperties()
            // use modules
            modules(myAppModules)
        }
    }
}

3. Retrieve your components from Koin

Once you have declared some modules and you have started Koin, how can you retrieve your instances in your Android Activity Fragments or Services?

3.1. Activity, Fragment & Service as KoinComponents

Activity, Fragment & Service are extended with the KoinComponents extension. You gain access to:

  • by inject() - lazy evaluated instance from Koin container

  • get() - eager fetch instance from Koin container

  • release() - release module’s instances from its path

  • getProperty()/setProperty() - get/set property

For a module that declares a 'presenter' component:

val androidModule = module {
    // a factory of Presenter
    factory { Presenter() }
}

We can declare a property as lazy injected:

Lazy inject a property
class DetailActivity : AppCompatActivity() {

    // Lazy injected Presenter instance
    override val presenter : Presenter by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
    }

Or we can just directly get an instance:

Get directly an instance
class DetailActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Retrieve a Presenter instance
        val presenter : Presenter = get()
    }

3.2. Need inject() and get() anywhere else?

If you need to inject() or get() an instance from another class, just tag it with KoinComponent interface.

4. Koin DSL Extension for Android

Below, the added keywords for Koin DSL.

4.1. Getting Android context inside a Module

The androidContext() & androidApplication()` functions allows you to get the Context instance in a Koin module, to help you simply write expression that requires the Application instance.

val appModule = module {

    // create a Presenter instance with injection of R.string.mystring resources from Android
    factory {
        MyPresenter(androidContext().resources.getString(R.string.mystring))
    }
}

5. Scope features for Android

The koin-android-scope project is dedicated to bring Android scope features to the existing Scope API.

5.1. Gradle setup

Choose the koin-android-scope dependency to add to your Gradle project (android or androix version):

// Add Jcenter to your repositories if needed
repositories {
    jcenter()
}
dependencies {
    // Scope for Android
    implementation 'org.koin:koin-android-scope:{revnumber}'
    // or Scope for AndroidX
    implementation 'org.koin:koin-androidx-scope:{revnumber}'
}

5.2. Taming the Android lifecycle

Android components are mainly managed by their lifecycle: we can’t directly instantiate an Activity nor a Fragment. The system make all creation and management for us, and make callbacks on methods: onCreate, onStart…​

That’s why we can’t describe our Activity/Fragment/Service in a Koin module. We need then to inject dependencies into properties and also respect the lifecycle: Components related to the UI parts must be released on soon as we don’t need them anymore.

Then we have:

  • long live components (Services, Data Repository …​) - used by several screens, never dropped

  • medium live components (user sessions …​) - used by several screens, must be dropped after an amount of time

  • short live components (views) - used by only one screen & must be dropped at the end of the screen

Long live components can be easily described as single definitions. For medium and short live components we can have several approaches.

In the case of MVP architecture style, the Presenter is a short live component to help/support the UI. The presenter must be created each time the screen is showing, and dropped once the screen is gone.

A new Presenter is created each time
class DetailActivity : AppCompatActivity() {

    // injected Presenter
    override val presenter : Presenter by inject()

We can describe it in a module:

  • as factory - to produce a new instance each time the by inject() or get() is called

val androidModule = module {

    // Factory instance of Presenter
    factory { Presenter() }
}
  • as scope - to produce an instance tied to a scope

val androidModule = module {

    scope(named("scope_id")) {
        scoped { Presenter() }
    }
}

Most of Android memory leaks comes from referencing a UI/Android component from a non Android component. The system keeps a reference on it and can’t totally drop it via garbage collection.

5.3. CurrentScope - a scope tied to your lifecycle

Koin gives the currentScope property already bound to your Android component lifecycle. On lifecycle’s end, it will close automatically.

To benefit from the currentScope, you have to declare a scope for your activity (see how we use the named() qualifier with our Activity type):

val androidModule = module {

    scope(named<MyActivity>()) {
        scoped { Presenter() }
    }
}
class MyActivity : AppCompatActivity() {

    // inject Presenter instance from current scope
    val presenter : Presenter by currentScope.inject()

5.4. Sharing instances between components with scopes

In a more extended usage, you can use a Scope instance across components. For example, if we need to share a UserSession instance.

First declare a scope definition:

module {
    // Shared user session data
    scope(named("session")) {
        scoped { UserSession() }
    }
}

When needed to begin use a UserSession instance, create a scope for it:

val ourSession = getKoin().createScope("ourSession",named("session"))

Then use it anywhere you need it:

class MyActivity1 : AppCompatActivity() {

    val userSession : UserSession by ourSession.inject()
}
class MyActivity2 : AppCompatActivity() {

    val userSession : UserSession by ourSession.inject()
}

or you can also inject it with Koin DSL. If a presenter need it:

class Presenter(val userSession : UserSession)

Just inject it into constructor, with the right scope id:

module {
    // Shared user session data
    scope(named("session")) {
        scoped { UserSession() }
    }

    // Inject UserSession instance from "session" Scope
    factory { (scopeId : ScopeID) -> Presenter(getScope(scopeId).get())}
}

When you have to finish with your scope, just close it:

val ourSession = getKoin().getScope("ourSession")
ourSession.close()

6. Architecture Components with Koin: ViewModel

The koin-android-viewmodel project is dedicated to bring Android Architecture ViewModel features.

6.1. Gradle setup

Choose the koin-android-viewmodel dependency to add to your Gradle project (android or androix version):

// Add Jcenter to your repositories if needed
repositories {
    jcenter()
}
dependencies {
    // ViewModel for Android
    implementation 'org.koin:koin-android-viewmodel:{revnumber}'
    // or ViewModel for AndroidX
    implementation 'org.koin:koin-androidx-viewmodel:{revnumber}'
}

6.2. ViewModel DSL

The koin-android-viewmodel introduces a new viewModel DSL keyword that comes in complement of single and factory, to help declare a ViewModel component and bind it to an Android Component lifecycle.

val appModule = module {

    // ViewModel for Detail View
    viewModel { DetailViewModel(get(), get()) }

}

Your declared component must at least extends the android.arch.lifecycle.ViewModel class. You can specify how you inject the constructor of the class and use the get() function to inject dependencies.

The viewModel keyword helps declaring a factory instance of ViewModel. This instance will be handled by internal ViewModelFactory and reattach ViewModel instance if needed.

The viewModel keyword can also let you use the injection parameters.

6.3. Injecting your ViewModel

To inject a ViewModel in an Activity, Fragment or Service use:

  • by viewModel() - lazy delegate property to inject a ViewModel into a property

  • getViewModel() - directly get the ViewModel instance

class DetailActivity : AppCompatActivity() {

    // Lazy inject ViewModel
    val detailViewModel: DetailViewModel by viewModel()
}

6.4. Shared ViewModel

One ViewModel instance can be shared between Fragments and their host Activity.

To inject a shared ViewModel in a Fragment use:

  • by sharedViewModel() - lazy delegate property to inject shared ViewModel instance into a property

  • getSharedViewModel() - directly get the shared ViewModel instance

Just declare the ViewModel only once:

val weatherAppModule = module {

    // WeatherViewModel declaration for Weather View components
    viewModel { WeatherViewModel(get(), get()) }
}

Note: a qualifier for a ViewModel will be handled as a ViewModel’s Tag

And reuse it in Activity and Fragments:

class WeatherActivity : AppCompatActivity() {

    /*
     * Declare WeatherViewModel with Koin and allow constructor dependency injection
     */
    private val weatherViewModel by viewModel<WeatherViewModel>()
}

class WeatherHeaderFragment : Fragment() {

    /*
     * Declare shared WeatherViewModel with WeatherActivity
     */
    private val weatherViewModel by sharedViewModel<WeatherViewModel>()
}

class WeatherListFragment : Fragment() {

    /*
     * Declare shared WeatherViewModel with WeatherActivity
     */
    private val weatherViewModel by sharedViewModel<WeatherViewModel>()
}

The Activity sharing its ViewModel injects it with by viewModel() or getViewModel(). Fragments are reusing the shared ViewModel with by sharedViewModel().

6.5. ViewModel and injection parameters

the viewModel keyword and injection API is compatible with injection parameters.

In the module:

val appModule = module {

    // ViewModel for Detail View with id as parameter injection
    viewModel { (id : String) -> DetailViewModel(id, get(), get()) }
}

From the injection call site:

class DetailActivity : AppCompatActivity() {

    val id : String // id of the view

    // Lazy inject ViewModel with id parameter
    val detailViewModel: DetailViewModel by viewModel{ parametersOf(id)}
}