Что такое Koin и как он работает в приложении для Android

Цель

В этой статье я представляю основы Koin и сравниваю его с другими фреймворками внедрения зависимостей. Пример приложения был создан с использованием Koin, Fast Android Networking и ViewModels вместе с LiveData, чтобы предоставить полезный пример использования Koin на практике. Цель состоит в том, чтобы опробовать внедрение зависимостей с помощью Koin, поэтому я сосредоточусь на этом аспекте, оставив все остальное в стороне.

На данный момент приложение позволит пользователям войти в систему, просмотреть основную информацию профиля и получить список последних выпусков на Spotify. Он будет основан на Spotify API, весь код доступен здесь.

Что такое коин?

По словам его создателей, Koin в значительной степени является легкой альтернативой другим более надежным фреймворкам (например, Dagger, Guice и Spring).

Прагматичный легкий фреймворк для внедрения зависимостей для разработчиков на Kotlin.

Однако, судя по этой ветке reddit, не все так просто.

Koin - это не инжектор зависимостей, а локатор сервисов с умным повторным трюком, который вы можете использовать для ручного внедрения зависимостей, шаблон будет непропорционально масштабироваться.

Для разработчиков это означает, что Koin должен быть отличной альтернативой другим фреймворкам, когда дело доходит до небольших проектов, с другой стороны, в более крупных есть риск того, что накладные расходы на генерацию кода Koin превзойдут возможные преимущества.

Но давайте посмотрим правде в глаза: совершенно очевидно, что более крупные, старые и более зрелые фреймворки являются наиболее разумным выбором, когда речь идет о больших проектах с десятками разработчиков на борту. Однако Koin кажется отличной альтернативой для приложений малого и среднего размера.

Скорость и накладные расходы - это одно, другие важные свойства - это простота использования и количество шаблонов, необходимых для запуска и работы приложения.

В этой статье я буду использовать Dagger для сравнения, так как это наиболее известный и наиболее широко используемый фреймворк DI для Android.

Архитектура приложения

Прежде чем начинать, нужно собрать архитектуру своего приложения.

Что ж, слово Архитектура может быть немного большим для этого проекта, но даже самое маленькое приложение требует своего рода дизайна. Я буду использовать упрощенную Чистую архитектуру с шаблоном MVVM для уровня представления и RxJava, чтобы как можно быстрее выйти из основного потока на уровне домена.

В этой архитектуре Koin поможет нам соблюдать принцип инверсии зависимостей и отделить наш код.

Где использовать коин

Все идет нормально. Однако эта статья не о разработке приложений, а об испытании новой инфраструктуры внедрения зависимостей, поэтому давайте сосредоточимся на том, как она используется в примере приложения.

Путь внедрения коина почти такой же, как и в архитектуре. Представления получают ViewModels, ViewModels получают Interactors, а Interactors вводят любой требуемый репозиторий.

Следующая часть статьи будет посвящена самому Koin. Используйте мой проект в случаях, когда что-то непонятно или в качестве примера. Полный код может быть полезен для понимания того, как все это работает.

Давайте начнем с Koin

Зависимости

Перво-наперво, давайте позаботимся о зависимостях Gradle

// Project's build.gralde
allprojects {
    repositories {
        jcenter()
    }
}
// Module's build.gradle
dependencies {
   def koin_version = "1.0.0-RC-1"
   
   
   // Just Koin
   implementation "org.koin:koin-android:$koin_version"
   
   // Support for Android scopes
   implementation "org.koin:koin-androidx-scope:$koin_version"
   
   // Support for Android ViewModels
   implementation "org.koin:koin-androidx-viewmodel:$koin_version"
}

Ключевые слова Koin DSL

Koin основан на своем DSL, который состоит из следующих ключевых слов:

  • module { } - создать модуль Koin или подмодуль (внутри модуля)
  • factory { } - предоставить определение factory bean
  • single { } - предоставить определение bean
  • get() - разрешить зависимость компонентов (нетерпеливо)
  • inject() - разрешить зависимость компонентов во время выполнения компонентов Android (ленивый)
  • bind - дополнительная привязка типа Kotlin для данного определения bean
  • getProperty() - разрешить свойство

Модули

Модули определяют способ создания наших внедренных зависимостей. Они являются хорошей заменой модулей / компонентов Dagger. Обратной стороной здесь является отсутствие ошибок времени компиляции, когда у некоторых зависимостей нет своего провайдера. Вы должны действительно ударить по стене пословиц, прежде чем вы поймете, что забыли об одном из поставщиков.

val viewModelModule = module {
    //Provides an instance of ViewModel and binds it to an Android Component lifecycle
    viewModel { LauncherViewModel(get()) }
}
val repositoryModule = module {
     //Provides a singleton instance
     single { LoginStatusRepository(get()) }
}
val interactorModule = module {
     //Provides a new instance for each call
     factory { LoginStatusInteractor() }
}

Помните, чтобы использовать модуль, вы должны вызвать startKoin() в своем подклассе Application.

class MyApplication : Application() {
    override fun onCreate() {
         startKoin(this, listOf(viewModelModule, repositoryModule), interactorModule)
    }
}

Ввести это

Инъекция в Koin также безумно проста. Разрешить зависимость ленивым способом так же просто, как использовать inject().

val loginStatusInteractor: LoginStatusInteractor by inject()
val launcherViewModel: LauncherViewModel by viewModel()

Это обеспечит ленивых делегатов для наших значений, которые затем будут лениво «вводить» запрошенные значения.

Если по какой-то причине вы предпочитаете с нетерпением получить свою зависимость, get() найдется для вас.

val loginStatusInteractor: LoginStatusInteractor = get()

Очевидно, также доступна инъекция конструктора. Параметры можно запросить в конструкторе и предоставить в модуле с помощью ключевого слова get() или просто создав их.

// Repository and Service are declared in a module
class ReleasesRepository(private val apiService: ApiService) {
    fun fetchReleases(accessToken: String) = apiService.getNewReleases("Bearer $accessToken")
}
// Inject constructor in a module with injection by get()
single { ReleasesRepository(get()) }
// Or just create the required object
single { ReleasesRepository(ApiService()) }

Отсутствие внедрения конструктора с автоматическим разрешением - это небольшая проблема, поскольку мы должны помнить, что объявляем наши конструкторы в модулях каждый раз, когда мы их создаем.

Объезд

Давайте теперь сделаем небольшой экскурс и сравним то, что мы достигли с помощью Koin, с тем, что нужно было бы сделать в Dagger, чтобы получить тот же результат:

// We can provide LoginStatusInteractor by constuctor injection
@Singleton class LoginStatusInteractor @Inject constructor() { }
// Vomponent
@Singleton @Component interface MyComponent {     
fun loginStatusInteractor(): LoginStatusInteractor
}
// Create dagger component 
val myComponent = DaggerMyComponent.create()
// Finally we can inject 
val interactor = myComponent.loginStatusInteractor()

Изменить: я вношу изменения в код благодаря u / Zhuinden, который показал мне, как правильно вводить Dagger. Кроме того, согласно этой статье ленивую инъекцию можно легко осуществить с помощью Dagger. Спасибо Reddit!

Именованные определения

Если вы хотите иметь два определения одного и того же типа, вы можете использовать именованные определения (они очень похожи на квалификаторы Dagger). Просто определите и запросите свои определения с каким-нибудь именем.

// Both return Scheduler type
single("IO_SCHEDULER") { Schedulers.io() }
single("MAIN_THREAD_SCHEDULER") { AndroidSchedulers.mainThread() 
// Get them later by calling a named get()
get("IO_SCHEDULER")
get("MAIN_THREAD_SCHEDULER")
// Or inject them by using named delegate
val io : Scheduler by inject(name = "IO_SCHEDULER")
val mainThread : Scheduler by inject(name = "MAIN_THREAD_SCHEDULER")

Тестирование

Любой хороший разработчик знает: «Если он не протестирован, он неисправен». К счастью, с Koin очень легко внедрить mock-объекты в наш тест. Просто замените свои модули в тестовом приложении на макетные модули, и все готово.

// Lazy delegate will be evaluated when called
val service : ApiService by inject()
@Test
fun myTest() {
    startKoin(myModules)
    // Close Koin when you are done with it for this test so that singletons can be recreated for the next one
    closeKoin()
}

Итак, что случилось с Коином?

Добро

  • Модули очень легко создавать и использовать
  • Поддержка ViewModels - это хорошо
  • Ленивая инъекция - всегда плюс
  • Провайдеры нейминга удобны и не требуют отдельного квалификатора для работы
  • Внедрение фиктивных модулей в тесты также легко
  • По сравнению с Dagger гораздо меньше шаблонов.
  • Понять это намного проще, чем Dagger

Плохо

  • Никакая инъекция автоконструктора - это облом
  • Отсутствие проверки деревьев зависимостей во время компиляции вынуждает программистов находить ошибки во время выполнения

Уродливый

  • Растущие накладные расходы делают Koin рискованным выбором для более крупных проектов. Может оказаться, что через несколько лет ваш проект станет слишком большим для вашего фреймворка внедрения зависимостей.

С Dagger есть определенные фиксированные накладные расходы, но тогда вам редко придется значительно изменять форму вашего графа, поскольку привязки автоматически распространяются по внедренным типам. При ручном внедрении зависимостей вам необходимо вручную распространять привязки на внедренные типы.

Еще хорошие новости! Только что вышла стабильная версия Koin (1.0.0). Ознакомьтесь с этой статьей создателя Koin. Это еще одна причина попробовать его в следующем проекте.