В последнее время многим программистам очень понравилась библиотека для реализации внедрения зависимостей Dagger2. Хотя, как мне кажется, из-за неочевидной работы под капотом и большим семейством аннотаций Dagger долго заходил в комьюнити. И так получается что сейчас куда не глянь многие использую эту библиотеку почти везде. И уже Dependancy Injection становится синонимом этой самой библиотеки. Хотя это всего лишь библиотека. Да, хорошая, не спорю. Статья будет не о свержение Dagger'a с трона короля библиотек DI. А я бы хотел рассказать о другом инструменте для подобных целей — это Koin.
Koin — это небольшая библиотека для написания внедрений зависимостей. Без прокси, кодогенерации и интроинспекции (introspection). Работает как Service Locator. Использует DSL и фичи языка Kotlin. Сама библиотека подразумевает, что будет использоваться в приложениях написанные на Kotlin, но можно и с Java.
Посмотрим как его можно использовать в проекте. Для начала надо реализовать модуль и все зависимости.
Рассмотрим, что же есть в Koin DSL.
applicationContext — Это лямбда для создание Koin модуля. Эта функция возвращает модуль Koin и является началом каждого определения компонента в Koin.
factory — Предоставление зависимости как фабричный компонент, т.е. создает каждый раз новый экземпляр.
bean — Предоставление зависимости как Singleton.
bind — Дополнительное связывание типа Kotlin для данного определения компонента.
get — Разрешает компонентные зависимости. Функция сама поймет какая зависимость требуется для каждого класса.
context — Обьявление логического контекста.
viewModel — Специальное предоставление зависимости для ViewModel, находится в отдельном пакете compile «org.koin:koin-android-architecture:$koin_version»
В конкретном примере мы работаем с архитектурными компонентами и используем ViewModel в проекте. У нас есть потребность инжектить IUserRepository во ViewModel. Koin позволяет довольно просто доставлять зависимости через конструктор во ViewModel.
Модуль необходимо будет запустить с помощью функции startKoin() в классе Application().
По факту этого уже нам хватит, чтобы использовать viewmodel в различных фрагментах и активити.
К тому же, используя by inject(), у нас происходит ленивая инициализация компонента.
Если мы против ленивых вещей, тогда можем сделать инициализировать напрямую:
Если вдруг вам надо поделиться своей ViewModel с Acitivity/Fragment, тогда можно использовать sharedViewModel(). В этот момет Acitivity или Fragment будут иметь один и тот же экземпляр MySharedViewModel.
Бывают случаи когда надо делать инжект например в кастомное вью, здесь вам поможет Koin Components. Достаточно отнаследоваться от KoinComponent и появится возможность использовать by inject<>(). На данный момент это не требуется в следующих классах: `Application`,`Context`, `Activity`, `Fragment`, `Service.
Для ViewMode ничего особенного, просто получаем необходимые зависимости в конструкторе.
Тут все просто, надо тестовый класс наследовать от KoinTest и появляется возможность инжектить прямо в тестовый класс.
Ошибки Koin будет выкидавать в runtime. Так что тестировать необходимо все.
В процессе дебага Koin делает логгирование и в случае ошибки кидает вполне понятный stacktrace:
На данный момент доступна версия 0.9.1, наверное с этим и связано малое распространение KOIN в проектах. Лично мне очень понравилась простота использования, возможность работы с ViewModel и ленивая инициализация компонентов. Думаю после релиза Koin ждет большая жизнь в Android/Kotlin разработке. А если вам не понравился Koin потому, что это Service Locator и Dagger тоже душу не греет, то тогда смотрите в сторону Kodein и Toothpick.
Что такое KOIN?
Koin — это небольшая библиотека для написания внедрений зависимостей. Без прокси, кодогенерации и интроинспекции (introspection). Работает как Service Locator. Использует DSL и фичи языка Kotlin. Сама библиотека подразумевает, что будет использоваться в приложениях написанные на Kotlin, но можно и с Java.
Посмотрим как его можно использовать в проекте. Для начала надо реализовать модуль и все зависимости.
// 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) } }
Зависимости
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) }
Рассмотрим, что же есть в Koin DSL.
applicationContext — Это лямбда для создание Koin модуля. Эта функция возвращает модуль Koin и является началом каждого определения компонента в Koin.
factory — Предоставление зависимости как фабричный компонент, т.е. создает каждый раз новый экземпляр.
bean — Предоставление зависимости как Singleton.
bind — Дополнительное связывание типа Kotlin для данного определения компонента.
get — Разрешает компонентные зависимости. Функция сама поймет какая зависимость требуется для каждого класса.
context — Обьявление логического контекста.
viewModel — Специальное предоставление зависимости для ViewModel, находится в отдельном пакете compile «org.koin:koin-android-architecture:$koin_version»
В конкретном примере мы работаем с архитектурными компонентами и используем ViewModel в проекте. У нас есть потребность инжектить IUserRepository во ViewModel. Koin позволяет довольно просто доставлять зависимости через конструктор во ViewModel.
Модуль необходимо будет запустить с помощью функции startKoin() в классе Application().
override fun onCreate() { super.onCreate() startKoin(this, listOf(mainModule, remoteDatasourceModule)) }
По факту этого уже нам хватит, чтобы использовать viewmodel в различных фрагментах и активити.
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()
К тому же, используя by inject(), у нас происходит ленивая инициализация компонента.
Если мы против ленивых вещей, тогда можем сделать инициализировать напрямую:
val myViewModel : MyViewModel = getViewModel()
Если вдруг вам надо поделиться своей ViewModel с Acitivity/Fragment, тогда можно использовать sharedViewModel(). В этот момет Acitivity или Fragment будут иметь один и тот же экземпляр MySharedViewModel.
Бывают случаи когда надо делать инжект например в кастомное вью, здесь вам поможет Koin Components. Достаточно отнаследоваться от KoinComponent и появится возможность использовать by inject<>(). На данный момент это не требуется в следующих классах: `Application`,`Context`, `Activity`, `Fragment`, `Service.
Для ViewMode ничего особенного, просто получаем необходимые зависимости в конструкторе.
// Use Repository - injected by constructor by Koin class MyViewModel(val repository : Repository) : ViewModel(){ .... }
Пример с BaseViewModel и 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 }
Тесты
Тут все просто, надо тестовый класс наследовать от KoinTest и появляется возможность инжектить прямо в тестовый класс.
Пример теста
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()) } }
Логирование.
Ошибки Koin будет выкидавать в runtime. Так что тестировать необходимо все.
В процессе дебага Koin делает логгирование и в случае ошибки кидает вполне понятный stacktrace:
Пример логирования при создании
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
Пример ошибки
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)
Заключение.
На данный момент доступна версия 0.9.1, наверное с этим и связано малое распространение KOIN в проектах. Лично мне очень понравилась простота использования, возможность работы с ViewModel и ленивая инициализация компонентов. Думаю после релиза Koin ждет большая жизнь в Android/Kotlin разработке. А если вам не понравился Koin потому, что это Service Locator и Dagger тоже душу не греет, то тогда смотрите в сторону Kodein и Toothpick.
Only registered users can participate in poll. Log in, please.
Что сейчас используете в своем проекте?
0.81%Dagger3
52.7%Dagger 2195
14.59%Koin54
5.14%Kodein19
8.65%Toothpick32
0%Tiger0
0%Transfuse0
0%Proton0
0.27%Feather1
10%У меня нет и намека на DI37
7.84%Пишу все руками29
370 users voted. 66 users abstained.