Not a dagger
- Tutorial
Recently, many programmers have really liked the library for implementing Dagger2 dependency injection. Although, it seems to me, because of the unobvious work under the hood and a large family of annotations, Dagger has been entering the community for a long time. And so it turns out that now wherever you look, many use this library almost everywhere. And already Dependancy Injection becomes a synonym for this very library. Although this is just a library. Yes, good, I do not argue. The article will not be about the overthrow of Dagger from the throne of the King of DI Libraries. And I would like to talk about another tool for such purposes - this is Koin .
Koin is a small library for writing dependency injections. Without proxies, code generation and introspection . Works like a service locator . Uses DSL and Kotlin language features. The library itself implies that it will be used in applications written in Kotlin, but it is also possible with Java .
Let's see how it can be used in the project. First you need to implement the module and all the dependencies.
Consider what is in the Koin DSL.
applicationContext - This is a lambda for creating a Koin module. This function returns the Koin module and is the beginning of each component definition in Koin.
factory - Providing the dependency as a factory component, i.e. creates a new instance each time.
bean - Providing dependencies as Singleton.
bind - Optional Kotlin type binding for this component definition.
get - Allows component dependencies. The function itself will understand what kind of dependency is required for each class.
context - The declaration of the logical context.
viewModel- A special provision of dependency for ViewModel is in a separate compile package “org.koin: koin-android-architecture: $ koin_version”.
In a specific example, we work with architectural components and use ViewModel in the project. We have a need to inject an IUserRepository into the ViewModel. Koin makes it fairly easy to deliver dependencies through the constructor in the ViewModel.
The module will need to be launched using the startKoin () function in the Application () class.
In fact, this is enough for us to use the viewmodel in various fragments and activities.
In addition, using by inject (), we have a lazy component initialization.
If we are against lazy things, then we can do initialize directly:
If you suddenly need to share your ViewModel with Acitivity / Fragment, then you can use sharedViewModel (). At this point, Acitivity or Fragment will have the same instance of MySharedViewModel.
There are cases when you need to do an injection, for example, in a custom view, Koin Components will help you here . It is enough to inherit from KoinComponent and it will be possible to use by inject <> (). This is not currently required in the following classes: `Application`,` Context`, `Activity`,` Fragment`, `Service.
For ViewMode, nothing special, just get the necessary dependencies in the constructor.
Everything is simple here, you need to inherit the test class from KoinTest and you can inject directly into the test class.
Errors Koin will throw in runtime. So everything needs to be tested.
During the debugging process, Koin does logging and, in case of an error, throws a completely understandable stacktrace:
Version 0.9.1 is currently available, probably due to this the small distribution of KOIN in projects is connected. Personally, I really liked the ease of use, the ability to work with ViewModel and lazy component initialization. I think after the release of Koin, a great life awaits in Android / Kotlin development. And if you didn’t like Koin because it is a Service Locator and Dagger doesn’t warm your soul either, then look towards Kodein and Toothpick.
What is KOIN?
Koin is a small library for writing dependency injections. Without proxies, code generation and introspection . Works like a service locator . Uses DSL and Kotlin language features. The library itself implies that it will be used in applications written in Kotlin, but it is also possible with Java .
Let's see how it can be used in the project. First you need to implement the module and all the dependencies.
// Koin module
val mainModule: Module = applicationContext {
viewModel { UserProfileViewModel(get()) }
viewModel { MyProfileViewModel(get()) }
viewModel { DisplayUsersViewModel(get()) }
viewModel { RegistrationViewModel(get()) }
bean { Cicerone.create().navigatorHolder }
bean { UserRepository(get(), get()) as IUsersRepository }
bean { createFirestore() }
}
val remoteDatasourceModule = applicationContext {
// provided web components
bean { createOkHttpClient() }
bean { createWebService(get(), SERVER_URL) }
}
Dependencies
fun createFirestore(): FirebaseFirestore {
val store = FirebaseFirestore.getInstance()
store.firestoreSettings = providesFirestoreSettings()
return store
}
fun providesFirestoreSettings(): FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(true)
.setSslEnabled(true)
.build()
fun createOkHttpClient(): OkHttpClient {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.readTimeout(TIMEOUT, TimeUnit.SECONDS)
.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
.build()
}
inline fun createWebService(okHttpClient: OkHttpClient, url: String): T {
val retrofit = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.client(okHttpClient)
.build()
return retrofit.create(T::class.java)
}
Consider what is in the Koin DSL.
applicationContext - This is a lambda for creating a Koin module. This function returns the Koin module and is the beginning of each component definition in Koin.
factory - Providing the dependency as a factory component, i.e. creates a new instance each time.
bean - Providing dependencies as Singleton.
bind - Optional Kotlin type binding for this component definition.
get - Allows component dependencies. The function itself will understand what kind of dependency is required for each class.
context - The declaration of the logical context.
viewModel- A special provision of dependency for ViewModel is in a separate compile package “org.koin: koin-android-architecture: $ koin_version”.
In a specific example, we work with architectural components and use ViewModel in the project. We have a need to inject an IUserRepository into the ViewModel. Koin makes it fairly easy to deliver dependencies through the constructor in the ViewModel.
The module will need to be launched using the startKoin () function in the Application () class.
override fun onCreate() {
super.onCreate()
startKoin(this, listOf(mainModule, remoteDatasourceModule))
}
In fact, this is enough for us to use the viewmodel in various fragments and activities.
class MyActivity : AppCompatActivity(){
// Inject MyPresenter
val presenter : MyPresenter by inject()
// or Inject MyViewModel
val myViewModel : MyViewModel by viewModel()
// or Sharing ViewModel
val mySharedViewModel : MySharedViewModel by sharedViewModel()
In addition, using by inject (), we have a lazy component initialization.
If we are against lazy things, then we can do initialize directly:
val myViewModel : MyViewModel = getViewModel()
If you suddenly need to share your ViewModel with Acitivity / Fragment, then you can use sharedViewModel (). At this point, Acitivity or Fragment will have the same instance of MySharedViewModel.
There are cases when you need to do an injection, for example, in a custom view, Koin Components will help you here . It is enough to inherit from KoinComponent and it will be possible to use by inject <> (). This is not currently required in the following classes: `Application`,` Context`, `Activity`,` Fragment`, `Service.
For ViewMode, nothing special, just get the necessary dependencies in the constructor.
// Use Repository - injected by constructor by Koin
class MyViewModel(val repository : Repository) : ViewModel(){
....
}
Example with BaseViewModel and BaseFragment
open class BaseViewModel : ViewModel(), LifecycleObserver {
val disposables = CompositeDisposable()
val loadingStatus = MutableLiveData()
fun addObserver(lifecycle: Lifecycle) {
lifecycle.addObserver(this)
}
fun removeObserver(lifecycle: Lifecycle) {
lifecycle.removeObserver(this)
}
override fun onCleared() {
disposables.dispose()
super.onCleared()
}
}
abstract class BaseFragment(viewModelClass: KClass) : Fragment() {
protected val viewModel: T by viewModelByClass(true, viewModelClass)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.addObserver(lifecycle)
}
override fun onDestroyView() {
viewModel.removeObserver(lifecycle)
super.onDestroyView()
}
@LayoutRes
protected abstract fun getLayoutRes(): Int
}
class UserProfileFragment : BaseFragment(UserProfileViewModel::class) {
....// Здесь вы уже можете сразу использовать viewModel
}
Tests
Everything is simple here, you need to inherit the test class from KoinTest and you can inject directly into the test class.
Test example
val localJavaDatasourceModule = applicationContext {
provide { LocalDataSource(JavaReader()) as WeatherDatasource }
}
val testRxModule = applicationContext {
// provided components
provide { TestSchedulerProvider() as SchedulerProvider }
}
val testApp = weatherApp + testRxModule + localJavaDatasourceModule
class ResultPresenterTest : KoinTest {
val view: ResultListContract.View = mock(ResultListContract.View::class.java)
val presenter: ResultListContract.Presenter by inject { mapOf(RESULT_VIEW to view) }
@Before
fun before() {
startKoin(testApp)
}
@After
fun after() {
closeKoin()
}
@Test
fun testDisplayWeather() {
presenter.getWeather()
Mockito.verify(view).displayWeather(emptyList())
}
}
Logging
Errors Koin will throw in runtime. So everything needs to be tested.
During the debugging process, Koin does logging and, in case of an error, throws a completely understandable stacktrace:
Logging Example on Creation
04-02 12:45:23.344 ? I/KOIN: [context] create
04-02 12:45:23.377 ? I/KOIN: [module] declare Factory[class=ru.a1024bits.bytheway.viewmodel.UserProfileViewModel, binds~(android.arch.lifecycle.ViewModel)]
[module] declare Factory[class=ru.a1024bits.bytheway.viewmodel.MyProfileViewModel, binds~(android.arch.lifecycle.ViewModel)]
[module] declare Factory[class=ru.a1024bits.bytheway.viewmodel.DisplayUsersViewModel, binds~(android.arch.lifecycle.ViewModel)]
[module] declare Factory[class=ru.a1024bits.bytheway.viewmodel.RegistrationViewModel, binds~(android.arch.lifecycle.ViewModel)]
[module] declare Bean[class=ru.a1024bits.bytheway.router.LocalCiceroneHolder]
[module] declare Bean[class=ru.terrakok.cicerone.NavigatorHolder]
[module] declare Bean[class=ru.a1024bits.bytheway.repository.IUsersRepository]
[module] declare Bean[class=com.google.firebase.firestore.FirebaseFirestore]
04-02 12:45:23.379 ? I/KOIN: [module] declare Bean[class=okhttp3.OkHttpClient]
[module] declare Bean[class=ru.a1024bits.bytheway.MapWebService]
[modules] loaded 10 definitions
[properties] load koin.properties
04-02 12:45:23.397 ? I/KOIN: [init] Load Android features
04-02 12:45:23.566 ? I/KOIN: [Properties] no assets/koin.properties file to load
[init] ~ added Android application bean reference
[module] declare Bean[class=android.app.Application, binds~(android.content.Context)]
04-02 12:45:23.593 ? I/KOIN: [ViewModel] get for FragmentActivity @ ru.a1024bits.bytheway.ui.activity.SplashActivity@3cd01a24
04-02 12:45:23.594 ? I/KOIN: Resolve class[ru.a1024bits.bytheway.viewmodel.RegistrationViewModel] with Factory[class=ru.a1024bits.bytheway.viewmodel.RegistrationViewModel, binds~(android.arch.lifecycle.ViewModel)]
04-02 12:45:23.596 ? I/KOIN: Resolve class[ru.a1024bits.bytheway.repository.IUsersRepository] with Bean[class=ru.a1024bits.bytheway.repository.IUsersRepository]
Resolve class[com.google.firebase.firestore.FirebaseFirestore] with Bean[class=com.google.firebase.firestore.FirebaseFirestore]
04-02 12:45:23.600 ? I/KOIN: (*) Created
04-02 12:45:23.601 ? I/KOIN: Resolve class[ru.a1024bits.bytheway.MapWebService] with Bean[class=ru.a1024bits.bytheway.MapWebService]
Resolve class[okhttp3.OkHttpClient] with Bean[class=okhttp3.OkHttpClient]
04-02 12:45:23.608 ? I/KOIN: (*) Created
04-02 12:45:23.615 ? I/KOIN: (*) Created
04-02 12:45:23.616 ? I/KOIN: (*) Created
(*) Created
04-02 12:45:23.749 ? I/KOIN: [ViewModel] get for FragmentActivity @ ru.a1024bits.bytheway.ui.activity.RegistrationActivity@187baf0
Resolve class[ru.a1024bits.bytheway.viewmodel.RegistrationViewModel] with Factory[class=ru.a1024bits.bytheway.viewmodel.RegistrationViewModel, binds~(android.arch.lifecycle.ViewModel)]
Resolve class[ru.a1024bits.bytheway.repository.IUsersRepository] with Bean[class=ru.a1024bits.bytheway.repository.IUsersRepository]
(*) Created
Error example
I/KOIN: Resolve class[ru.a1024bits.bytheway.viewmodel.RegistrationViewModel] with Factory[class=ru.a1024bits.bytheway.viewmodel.RegistrationViewModel, binds~(android.arch.lifecycle.ViewModel)]
W/System.err: org.koin.error.NoBeanDefFoundException: No definition found to resolve type 'ru.a1024bits.bytheway.repository.UserRepository'.
Check your module definition
W/System.err: at org.koin.KoinContext.getVisibleBeanDefinition(KoinContext.kt:119)
at org.koin.KoinContext.resolveInstance(KoinContext.kt:77)
at ru.a1024bits.bytheway.koin.ModuleKt$mainModule$1$4.invoke(Module.kt:39)
at ru.a1024bits.bytheway.koin.ModuleKt$mainModule$1$4.invoke(Unknown Source:2)
at org.koin.core.instance.InstanceFactory.createInstance(InstanceFactory.kt:58)
at org.koin.core.instance.InstanceFactory.retrieveInstance(InstanceFactory.kt:22)
at org.koin.KoinContext$resolveInstance$$inlined$synchronized$lambda$1.invoke(KoinContext.kt:85)
at org.koin.KoinContext$resolveInstance$$inlined$synchronized$lambda$1.invoke(KoinContext.kt:23)
at org.koin.ResolutionStack.resolve(ResolutionStack.kt:23)
at org.koin.KoinContext.resolveInstance(KoinContext.kt:80)
at org.koin.android.architecture.ext.KoinExtKt.getWithDefinitions(KoinExt.kt:56)
at org.koin.android.architecture.ext.KoinExtKt.getByTypeName(KoinExt.kt:32)
at org.koin.android.architecture.ext.KoinExtKt.get(KoinExt.kt:66)
at org.koin.android.architecture.ext.KoinFactory.create(KoinFactory.kt:31)
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:134)
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:102)
at ru.a1024bits.bytheway.ui.activity.SplashActivity.onCreate(SplashActivity.kt:56)
Conclusion
Version 0.9.1 is currently available, probably due to this the small distribution of KOIN in projects is connected. Personally, I really liked the ease of use, the ability to work with ViewModel and lazy component initialization. I think after the release of Koin, a great life awaits in Android / Kotlin development. And if you didn’t like Koin because it is a Service Locator and Dagger doesn’t warm your soul either, then look towards Kodein and Toothpick.
Only registered users can participate in the survey. Please come in.
What are you using in your project now?
- 1% Dagger 3
- 54.9% Dagger 2 161
- 10.5% Koin 31
- 5.8% Kodein 17
- 9.2% Toothpick 27
- 0% Tiger 0
- 0% Transfuse 0
- 0% Proton 0
- 0.3% Feather 1
- 9.5% I don’t have a hint of DI 28 either
- 8.5% I write everything with my hands 25