Лучший опыт

Внедрение зависимостей в Android с помощью Koin.

Koin Внедрение зависимостей (Dependency Injection, DI) можно определить как один из пяти принципов SOLID. Этот принцип помогает управлять зависимостями. Dagger-Hilt  —  первое, что приходит на ум большинству из нас в связи с этой темой. Dagger-Hilt, разработанный Google, позволяет эффективно внедрять зависимости в Android-приложениях. Однако Koin  —  инструмент, о котором здесь пойдет речь,  —  справляется с этой задачей довольно хорошо. Что такое Koin Koin  —  эт
Внедрение зависимостей в Android с помощью Koin...

Koin

Внедрение зависимостей (Dependency Injection, DI) можно определить как один из пяти принципов SOLID. Этот принцип помогает управлять зависимостями. Dagger-Hilt  —  первое, что приходит на ум большинству из нас в связи с этой темой. Dagger-Hilt, разработанный Google, позволяет эффективно внедрять зависимости в Android-приложениях. Однако Koin  —  инструмент, о котором здесь пойдет речь,  —  справляется с этой задачей довольно хорошо.

Что такое Koin

Koin  —  это легковесный DI-фреймворк для Kotlin-разработчиков. Написанный на языке Kotlin, он поддерживает функцию Kotlin DSL (domain-specific language  —  предметно-ориентированный язык). Освоение этого простого DI-фреймворка не требует много сил и времени. По сравнению с Dagger-Hilt, Koin гораздо проще в использовании. Главное отличие между этими фреймворками заключается в том, что в Dagger-Hilt внедрение зависимостей происходит во время компиляции, а в Koin  —  во время выполнения. Именно поэтому Dagger-Hilt более широко используется на практике. Однако, учитывая, что Koin также довольно популярен в среде разработчиков, его следует изучить.

Итак, погрузимся в Koin и посмотрим пример на его основе. Напишем простое приложение для получения данных из API JSONPlaceholder.

Вот пример.

Шаг 1. Добавление зависимости в build.gradle

// Корутины
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"

// Koin
implementation "io.insert-koin:koin-android:$koin_version"
implementation "io.insert-koin:koin-androidx-navigation:$koin_version"
testImplementation "io.insert-koin:koin-test-junit4:$koin_version"

// Retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

Шаг 2. Создание класса данных MyModelItem

Создадим класс модели для ответа. Он представляет собой ответ от API.

data class MyModelItem(
@SerializedName("body")
val body: String,
@SerializedName("id")
val id: Int,
@SerializedName("title")
val title: String,
@SerializedName("userId")
val userId: Int
)

Шаг 3. Определение интерфейса MyApi

Создадим GET-запрос, а также функцию приостановки (suspend function).

interface MyApi {
@GET("posts")
suspend fun doNetworkCall(): Response<List<MyModelItem>>
}

Шаг 4. Создание файла AppModule

По сравнению с Dagger-Hilt, создание синглтона в Koin выполняется намного проще с помощью функции области видимости (scope function). Внутри AppModule можно напрямую получить ранее внедренный объект с помощью метода get(). Можно также использовать viewModel для создания области видимости ViewModel.

val appModule = module {
// область видимости синглтона
single {
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MyApi::class.java)
}

single<MyRepository> {
MyRepositoryImpl(get())
}

viewModel {
MyViewModel(get())
}
}

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

val appModule = module {
factory{
...
}
}

Шаг 5. Класс данных Resource

Мы создали класс данных Resource, который позволяет возвращать статус любого типа. Теперь можно изменить возврат в репозитории (Repository).

data class Resource<out T>(val status: Status, val data: T?, val message: String?) {

companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}

enum class Status {
SUCCESS,
ERROR,
LOADING
}

Шаг 6. Интерфейс MyRepository

interface MyRepository {
suspend fun doNetworkCall(): Resource<List<MyModelItem>>
}

Шаг 7. Класс MyRepositoryImpl

class MyRepositoryImpl(private val api: MyApi) : MyRepository{
override suspend fun doNetworkCall(): Resource<List<MyModelItem>> {
return try {
val response = api.doNetworkCall()
if(response.isSuccessful){
response.body()?.let {
return@let Resource.success(it)
} ?: Resource.error("Error", null)
}else{
Resource.error("Error", null)
}
}catch (e: Exception){
Resource.error("No Data!", null)
}
}
}

Шаг 8. Класс MyViewModel

Можем получать данные и наблюдать их в Activity с помощью LiveData.

class MyViewModel(
private val repository: MyRepository
) : ViewModel() {

val myModelItem = MutableLiveData<Resource<List<MyModelItem>>>()

fun doNetworkCall(){
CoroutineScope(Dispatchers.IO).launch {
val resource = repository.doNetworkCall()
withContext(Dispatchers.Main){
resource.data?.let {
myModelItem.value = resource
}
}
}
}
}

Шаг 9. Создание класса Application

Мы еще не инициализировали Koin. Когда приложение будет создано, проведите его инициализацию. Сначала нужно предоставить модуль.

class MyApplication: Application(){
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(appModule)
}
}
}

Не забудьте добавить класс Application в файл AndroidManifest.xml.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name=".MyApplication"
.
.
.
</application>

</manifest>

Шаг 10. MainActivity

Теперь остается только внедрить ViewModel и наблюдать за данными!

class MainActivity : AppCompatActivity(), AndroidScopeComponent {

override val scope: Scope by activityScope()
private val viewModel by viewModel<MyViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

viewModel.doNetworkCall()
observeLiveData()
}

private fun observeLiveData(){
viewModel.myModelItem.observe(this) { item ->
item?.let {items->
items.data?.forEach {item->
println("id: ${item.id}")
println("body: ${item.body}")
println("title: ${item.title}")
println("userId: ${item.userId}")
}
}
}
}
}

В общем, по сравнению с Dagger-Hilt, Koin действительно прост в использовании. Однако, как уже упоминалось, у него есть и некоторые недостатки. Решение за вами!