Arnaud Giuliani <arnaud.g@insert-koin.io> - and on twitter: @arnogiu :revnumber: 1.0.0-RC-1 :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:

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin(this, 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:

// Start Koin and init Application instance definition
startKoin(androidContext, myAppModules)

Also, there is a with operator that can help you bind the Context instance in the case of non Android class (Junit for example). It can help like follow:

Example of mocking Androic context in a JUnit
// Start Koin and init Context instance definition
startKoin(myAppModules) with (mock(Context::class.java))

2.3. Koin Logging

With the startKoin() function, we have a parameter logger which has a default value: 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(this, myAppModules, logger = EmptyLogger())
    }
}

2.4. Properties

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

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

    override fun onCreate() {
        super.onCreate()

        startKoin(this, myAppModules, extraProperties = mapOf( ... ))
    }
}

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() function 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 { Presenter() }
}

// Resolve the Presenter instance with a previously created scope
get<Presenter>(scope = /* your scope */)

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

5.3. Activity & Fragment scopes

You can retrieve (get or create) the scope associated from your Activity or Fragment with the getCurrentScope() function.

5.4. Binding scope to lifecycle

Koin gives the bindScope function to bind the actual Android component lifecycle, to a given scope. On lifecycle’s end, this will close the bound scope.

class MyActivity : AppCompatActivity() {

    // inject Presenter instance, tied to current MyActivity's scope
    val presenter : Presenter by inject(scope = getCurrentScope())

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

        // bind current lifecycle to Activity's scope
        bindScope(getCurrentScope())
    }

5.5. 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 { UserSession() }
}

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

getKoin().createScope("session")

Then use it anywhere you need it:

class MyActivity1 : AppCompatActivity() {

    // inject Presenter instance, tied to current MyActivity's scope
    val userSession : UserSession by inject(scope = getKoin().getScope("session"))
}
class MyActivity2 : AppCompatActivity() {

    // inject Presenter instance, tied to current MyActivity's scope
    val userSession : UserSession by inject(scope = getKoin().getScope("session"))
}

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 teh right scope:

module {
    // Shared user session data
    scope { UserSession() }
    // Inject UserSession instance from "session" Scope
    factory { Presenter(get(scope = getScope("session")))}
}

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

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

6. Architecture Components with Koin: ViewModel

The koin-android-viewmodel project is dedicated to bring Android Architecture VieModel 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()) }
    // or
    viewModel<DetailViewModel>()

}

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 help declare 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 viewModel: 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()) }
    // or
    viewModel<WeatherViewModel>()
}

And reuse it in Activity and Fragments:

class WeatherActivity : AppCompatActivity() {

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

class WeatherHeaderFragment : Fragment() {

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

class WeatherListFragment : Fragment() {

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

The Activity sharing its ViewModel inject 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 viewModel: DetailViewModel by viewModel{ parametersOf(id)}
}