Hinweis:
Dieser Blogartikel ist älter als 5 Jahre – die genannten Inhalte sind eventuell überholt.
For many years now, there was basically only one dependency injection library used in Android app projects: Dagger (2). While Dagger is offering all the features you could ask for and the API has improved a lot with the update from version 1 to 2, it is still quite tedious to set up. Plus—at least for me—it never felt like a very simple and elegant way of satisfying my dependency injection needs. Enter Koin.
With Kotlin gaining more and more popularity (especially among Android developers), a new option for dependency injection has risen: Koin. Koin has existed for a while now, but the first stable version was released quite recently.
Disclaimer: Koin may be used for all kinds of applications, but this article’s focus is on Android app projects (for which Koin offers several bonus features—as described below).
In order to try Koin for myself, I have forked the Kotlin MVP implementation of the famous ToDo App from Google’s Android Architecture Samples and integrated Koin in order to provide the dependencies. The resulting source code can be found here. Note that the author of Koin has done something very similar a couple of months ago, but since then, Koin has changed a bit plus I wanted to try the transition for myself.
Koin can make your life quite a bit easier, especially if you’re already using Kotlin. So, let’s take a look at the five reasons to use Koin:
Koin is Easy to Set up
Getting started with Koin could hardly be easier (see Getting Started for Android). All you have to do is:
- Add the dependency in your build.gradle:
1implementation "org.koin:koin-android:1.0.1" - Create the first module:
1234567val repositoryModule = module {single { ToDoDatabase.getInstance(androidContext()) }}val appModules = listOf(repositoryModule)
(Note that the Android Context is directly usable as a parameter through Koin’s Android extension) - Start Koin in your Application class (which needs to be set up in the AndroidManifest.xml, of course):
1234567891011class ToDoApplication : Application() {override fun onCreate() {super.onCreate()startKoin(this, appModules)}} - Start injecting:
12345class TasksActivity : AppCompatActivity() {val todoDatabase by inject<ToDoDatabase>()}
That’s it. Now you can already use the database in your activity.
Of course, you actually might prefer applying a pattern like MVP or MVVM to keep stuff like database transactions out of your Android activity/fragment/service etc. classes. In case you want to use a Koin-provided dependency in a non-Android class (to be specific, any class not implementing ComponentCallbacks), there’s a small extra step: implement the interface KoinComponent to get access to functions like inject() :
1 2 3 4 5 |
class TasksPresenter : KoinComponent { val todoDatabase by inject<ToDoDatabase>() } |
Kotlin Features—Elegant DSL and Delegated Properties
As you may have seen above, Koin uses its own DSL instead of annotations and code generation (like Dagger). Here, Kotlin’s language features really shine:
1 2 3 4 5 6 7 8 9 |
val appModule = module { single { ToDoDatabase.getInstance(androidContext()) } factory { TasksFragment() } factory<TasksContract.Presenter> { TasksPresenter(get()) } } |
Basically, there are two ways of creating instances— single creates singletons (like @Singleton in Dagger) while factory creates a new instance every time an object of the respective class is injected. In order to match another types, both single and factory can use type attributes (there are other ways of dealing with generics as well).
In the example, the TasksPresenter ’s constructor would take one parameter of type ToDoDatabase . Since we have defined a single for ToDoDatabase above, this parameter can easily be resolved by using the get() function.
To me, this elegant way of defining modules is definitely an improvement over code generation—having to hit “build“ before being able to use a newly created module/component can become quite tedious in large projects. Using delegated properties for injection feels like a very natural thing to do in Kotlin. By the way, there are two ways of providing the type: via a type parameter or via an explicit type definition:
1 2 3 |
val todoDatabase by inject<ToDoDatabase>() val anotherTodoDatabase : ToDoDatabase by inject() |
One thing to mention here: inject() is lazy (unless you configure your module or dependency differently)—which is typically what I want when injecting dependencies—while get() is eager.
Android Features
As mentioned above, the Android context is directly available in modules via an extension function. In addition to this, Koin provides several features and quite a bit of documentation especially for Android apps:
- ScopesIn Android, many dependencies typically need to be available during the lifetime of Android components like activities or fragments. Just creating new instances of dependencies via
factory may be problematic since this can cause memory leaks (e.g. if a presenter keeps a reference to an Android view/fragment). To avoid that, Koin offers scopes that link a dependency’s availability to the lifecycle of an Android component.
To use scopes, you need to include a separate module (note that there is another one for AndroidX):
1implementation "org.koin:koin-android-scope:1.0.1"Defining dependencies using scopes is just as easy as using single or factory :
1234567val appModule = module {scope("TaskDetailActivity") { TaskDetailFragment() }scope<TaskDetailContract.Presenter>("TaskDetailActivity") { TaskDetailPresenter(getProperty(Properties.EXTRA_TASK_ID)) }}Now both dependencies are available in the scope called “TaskDetailActivity“ – all that is left to do is to bind this scope to the activity’s lifecycle:
123456789101112131415class TaskDetailActivity : AppCompatActivity() {private val taskDetailFragment: TaskDetailFragment by inject()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.taskdetail_act)bindScope(getOrCreateScope("TaskDetailActivity"))}}That’s it. Now the dependencies will only exist as long as the activity.
- Architecture Components and ViewModel
Koin offers another module for using ViewModels:
1implementation "org.koin:koin-android-viewmodel:1.0.1"While this sample is using MVP we can also easily inject dependencies in our ViewModels.
All you need is a ViewModel with a dependency like this
1class TaskDetailViewModel(private val taskRepository: TaskRepository) {}and this line in your module setup.
1viewModel { TaskDetailViewModel(get()) }Now you can use your ViewModel in your view like this:
123456789override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_task_detail)viewModel = getViewModel()}
Testing
When testing your application, you might want to replace some dependencies with mock instances simulating whatever behavior you need in your tests—this is one of the main advantages of using dependency injection, after all. You might have guessed it—Koin provides a module for testing.
1 |
testCompile 'org.koin:koin-test:1.0.1' |
Then, all you have to do is extend KoinTest and start injecting:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class TasksPresenterKoinTest : KoinTest { val presenter: TasksContract.Presenter by inject() @Before fun setUp() { startKoin(appModules) } @After fun tearDown() { stopKoin() } } |
Of course, you can exchange modules easily in startKoin() ; but if you want to mock only some classes, Koin has you covered:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@Test fun showLoadingIndicatorWhileTasksAreRefreshed() { // given a repository and a view declareMock<TasksRepository>() declareMock<TasksContract.View>() val mockRepository = get<TasksRepository>() val mockView = get<TasksContract.View>() presenter.view = mockView // when tasks are loaded presenter.loadTasks(true) // then loading indicator is shown and tasks are refreshed verify(mockView).setLoadingIndicator(true) verify(mockRepository).refreshTasks() } |
The function declareMock() creates a Mockito mock of the respective dependency. In the test above, the presenter uses a repository and a view, both of which shall be mocked for this case. After this is set up, we can call the function we want to test and verify the expected outcomes. More sophisticated setup (like having the repository return specific tasks) and verification (like capturing and checking arguments) can easily be added using the established Mockito APIs.
In case your dependency tree grows more complex, you might also want to check your modules.
Just like the other Koin features described above, unit testing was very easy to set up and I liked the neat possibility for creating mocks.
Logging
Last but not least, Koin logs a lot of information—the modules and dependency declarations as well as information about the creation of dependencies (since they are typically injected lazily) and their transitive dependencies.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
I/KOIN: [context] create I/KOIN: [module] declare Single [name='ToDoDatabase',class='com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase'] [module] declare Single [name='TasksDao',class='com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao'] I/KOIN: [module] declare Single [name='remoteDataSource',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource'] [module] declare Single [name='localDataSource',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource'] [module] declare Single [name='TasksRepository',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository'] I/KOIN: [module] declare Factory [name='TasksFragment',class='com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment'] [module] declare Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.Presenter'] I/KOIN: [module] declare Factory [name='TaskDetailFragment',class='com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailFragment'] [module] declare Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailContract.Presenter'] [module] declare Factory [name='AddEditTaskFragment',class='com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskFragment'] [module] declare Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskContract.Presenter'] I/KOIN: [module] declare Factory [name='StatisticsFragment',class='com.example.android.architecture.blueprints.todoapp.statistics.StatisticsFragment'] [module] declare Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.statistics.StatisticsContract.Presenter'] [modules] loaded 13 definitions D/KOIN: [modules] loaded in 12.056094 ms I/KOIN: [init] declare Android Context [module] declare Single [class='android.content.Context'] I/KOIN: [module] declare Single [class='android.app.Application'] I/KOIN: +-- 'com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment' D/KOIN: |-- [Factory [name='TasksFragment',class='com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment']] D/KOIN: |-- TasksFragment{a76f444} I/KOIN: \-- (*) Created D/KOIN: !-- [com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment] resolved in 9.344479 ms I/KOIN: +-- 'com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.Presenter' D/KOIN: |-- [Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.Presenter']] D/KOIN: |-- com.example.android.architecture.blueprints.todoapp.tasks.TasksPresenter@345ceae I/KOIN: \-- (*) Created D/KOIN: !-- [com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.Presenter] resolved in 2.499323 ms I/KOIN: +-- 'com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository' D/KOIN: |-- [Single [name='TasksRepository',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository']] I/KOIN: | +-- 'com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource' D/KOIN: | |-- [Single [name='remoteDataSource',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource']] D/KOIN: | |-- com.example.android.architecture.blueprints.todoapp.data.FakeTasksRemoteDataSource@246344f I/KOIN: | \-- (*) Created D/KOIN: | !-- [com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource] resolved in 2.057084 ms I/KOIN: | +-- 'com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource' D/KOIN: | |-- [Single [name='localDataSource',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource']] I/KOIN: | | +-- 'com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao' D/KOIN: | | |-- [Single [name='TasksDao',class='com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao']] I/KOIN: | | | +-- 'com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase' D/KOIN: | | | |-- [Single [name='ToDoDatabase',class='com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase']] I/KOIN: | | | | +-- 'android.content.Context' D/KOIN: | | | | |-- [Single [class='android.content.Context']] | | | | |-- com.example.android.architecture.blueprints.todoapp.ToDoApplication@fee87dc I/KOIN: | | | | \-- (*) Created D/KOIN: | | | | !-- [android.content.Context] resolved in 0.868438 ms D/KOIN: | | | |-- com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase_Impl@61d10e5 I/KOIN: | | | \-- (*) Created D/KOIN: | | | !-- [com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase] resolved in 7.766406 ms D/KOIN: | | |-- com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao_Impl@ba427ba I/KOIN: | | \-- (*) Created D/KOIN: | | !-- [com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao] resolved in 10.958333 ms | |-- com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource@cdbc66b I/KOIN: | \-- (*) Created D/KOIN: | !-- [com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource] resolved in 14.813802 ms |-- com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository@1ca45c8 I/KOIN: \-- (*) Created D/KOIN: !-- [com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository] resolved in 20.30526 ms |
This may sound simple, but it can be extremely helpful as the dependency tree grows more complex—especially with multiple build variants replacing modules or (of course!) tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
2018-09-19 15:21:00:675 (KOIN)::[i] [PrintLogger] display debug = false 2018-09-19 15:21:00:704 (KOIN)::[i] [context] create 2018-09-19 15:21:00:771 (KOIN)::[i] [module] declare Single [name='ToDoDatabase',class='com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase'] 2018-09-19 15:21:00:772 (KOIN)::[i] [module] declare Single [name='TasksDao',class='com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao'] 2018-09-19 15:21:00:772 (KOIN)::[i] [module] declare Single [name='remoteDataSource',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource'] 2018-09-19 15:21:00:772 (KOIN)::[i] [module] declare Single [name='localDataSource',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource'] 2018-09-19 15:21:00:772 (KOIN)::[i] [module] declare Single [name='TasksRepository',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository'] 2018-09-19 15:21:00:818 (KOIN)::[i] [module] declare Factory [name='TasksFragment',class='com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment'] 2018-09-19 15:21:00:819 (KOIN)::[i] [module] declare Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.Presenter'] 2018-09-19 15:21:00:819 (KOIN)::[i] [module] declare Scope [name='TaskDetailFragment',class='com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailFragment'] 2018-09-19 15:21:00:821 (KOIN)::[i] [module] declare Scope [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailContract.Presenter'] 2018-09-19 15:21:00:821 (KOIN)::[i] [module] declare Factory [name='AddEditTaskFragment',class='com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskFragment'] 2018-09-19 15:21:00:822 (KOIN)::[i] [module] declare Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskContract.Presenter'] 2018-09-19 15:21:00:824 (KOIN)::[i] [module] declare Factory [name='StatisticsFragment',class='com.example.android.architecture.blueprints.todoapp.statistics.StatisticsFragment'] 2018-09-19 15:21:00:826 (KOIN)::[i] [module] declare Factory [name='Presenter',class='com.example.android.architecture.blueprints.todoapp.statistics.StatisticsContract.Presenter'] 2018-09-19 15:21:00:826 (KOIN)::[i] [modules] loaded 13 definitions 2018-09-19 15:21:00:889 (KOIN)::[i] [mock] declare mock for class com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository 2018-09-19 15:21:00:894 (KOIN)::[i] [module] override Single [name='TasksRepository',class='com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository'] 2018-09-19 15:21:00:895 (KOIN)::[i] [modules] loaded 13 definitions 2018-09-19 15:21:00:896 (KOIN)::[i] [mock] declare mock for interface com.example.android.architecture.blueprints.todoapp.tasks.TasksContract$View 2018-09-19 15:21:00:899 (KOIN)::[i] [module] declare Single [name='View',class='com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.View'] 2018-09-19 15:21:00:899 (KOIN)::[i] [modules] loaded 14 definitions 2018-09-19 15:21:00:932 (KOIN)::[i] +-- 'com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository' 2018-09-19 15:21:02:667 (KOIN)::[i] \-- (*) Created 2018-09-19 15:21:02:667 (KOIN)::[i] +-- 'com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.View' 2018-09-19 15:21:02:982 (KOIN)::[i] \-- (*) Created 2018-09-19 15:21:02:982 (KOIN)::[i] +-- 'com.example.android.architecture.blueprints.todoapp.tasks.TasksContract.Presenter' 2018-09-19 15:21:02:985 (KOIN)::[i] \-- (*) Created 2018-09-19 15:21:02:992 (KOIN)::[i] +-- 'com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository' 2018-09-19 15:21:03:015 (KOIN)::[i] [Close] Closing Koin context |
Note the lines about mock declarations.
For everyone who has at some point struggled to understand the relationship between injected dependencies (which is probably just about anyone who has used dependency injection in a larger project), this should help a lot. Of course, Koin includes many features I didn’t mention above (the feature set is quite impressive for a 1.0), but as a new user, those were the parts I enjoyed the most when working with Koin.
Conclusion
Most of all, Koin is easy to use and integrates well in a Kotlin Android project (btw, it may be used for Java projects as well). With the 1.0.0 only released a short while ago, the project feels very mature and stable; also the documentation is pretty extensive.
Compared to Dagger 2, using Koin felt a lot easier, more fun and so far I haven’t encountered any missing features. So go ahead and try it!