Не Dagger’ом едины

в 7:53, , рубрики: android development, dagger 2, koin, kotlin, Разработка под android

В последнее время многим программистам очень понравилась библиотека для реализации внедрения зависимостей 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(){
     ....
}

Пример с 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.

Автор: 1024bita

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js