📜 ⬆️ ⬇️

Not by Dagger

Recently, many programmers have really liked the library for implementing dependency injection Dagger2. Although, as it seems to me, due to non-obvious work under the hood and a large family of annotations, Dagger has been in the community for a long time. And so it turns out that now where do not look many use this library almost everywhere. And already Dependancy Injection becomes synonymous with this very library. Although it 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 the DI libraries. And I would like to talk about another tool for such purposes - this is Koin .

What is KOIN?


Koin is a small library for writing dependency injection. Without proxy, code generation and introspection . Works as a Service Locator . Uses DSL and features of the Kotlin language. 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 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<MapWebService>(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 <reified T> 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 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 dependencies as a factory component, i.e. creates a new instance each time.
bean - Providing dependencies as a singleton.
bind - Optional binding of the Kotlin type for this component definition.
get - Enables component dependencies. The function itself will understand what dependency is required for each class.
context - The declaration of a logical context.
viewModel - Special dependency provision for ViewModel, is in a separate compile "org.koin: koin-android-architecture: $ koin_version" package

In the specific example, we work with architectural components and use the ViewModel in the project. We need to inject the IUserRepository into the ViewModel. Koin makes it quite easy to deliver dependencies through the constructor in the ViewModel.

The module will need to be started 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 activations.

 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 the initialization directly:

 val myViewModel : MyViewModel = getViewModel() 

If suddenly you need to share your ViewModel with Acitivity / Fragment, then you can use sharedViewModel (). In this momet Acitivity or Fragment will have the same MySharedViewModel instance.

There are cases when it is necessary to do inject, for example, in a custom twist, Koin Components will help you. It is enough to inherit from KoinComponent and it will be possible to use by inject <> (). Currently this is not 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<Boolean>() fun addObserver(lifecycle: Lifecycle) { lifecycle.addObserver(this) } fun removeObserver(lifecycle: Lifecycle) { lifecycle.removeObserver(this) } override fun onCleared() { disposables.dispose() super.onCleared() } } abstract class BaseFragment<out T : BaseViewModel>(viewModelClass: KClass<T>) : 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>(UserProfileViewModel::class) { ....//       viewModel } 


Tests


Everything is simple, you need to inherit the test class from KoinTest and it is possible to inject directly into the test class.

Sample test
 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


Koin errors will be thrown out at runtime. So you need to test everything.
During the debug process, Koin logs and, in case of an error, throws a clear stacktrace:

An example of logging when creating
 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


At the moment, version 0.9.1 is available, probably this is the reason for the small distribution of KOIN in projects. Personally, I really liked the ease of use, the ability to work with the ViewModel and lazy initialization of components. I think after the release of Koin waiting for a big life in the Android / Kotlin development. And if you don’t like Koin because it’s a Service Locator and Dagger doesn’t warm your soul either, then look towards Kodein and Toothpick.

Source: https://habr.com/ru/post/352352/


All Articles