В последнее время многим программистам очень понравилась библиотека для реализации внедрения зависимостей Dagger2. Хотя, как мне кажется, из-за неочевидной работы под капотом и большим семейством аннотаций Dagger долго заходил в комьюнити. И так получается что сейчас куда не глянь многие использую эту библиотеку почти везде. И уже Dependancy Injection становится синонимом этой самой библиотеки. Хотя это всего лишь библиотека. Да, хорошая, не спорю. Статья будет не о свержение Dagger'a с трона короля библиотек DI. А я бы хотел рассказать о другом инструменте для подобных целей — это Koin.
Что такое 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(){
....
}
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.
Автор: 1024bita