Как рефлексия в Kotlin помогает автоматизировать работу с Koin

Работая над большим многомодульным проектом, я нередко попадаю в ситуацию, когда забываю добавить новый модуль в startKoin, из-за чего ловлю org.koin.core.error.NoDefinitionFoundException — отсутствие объявления типа, инъекцию которого пытается сделать Koin, и поэтому, так как, на мой взгляд, главная концепция IT — автоматизация нашей жизни, неплохо было бы автоматизировать и этот аспект.

if (Koin!= Hilt) return

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

sealed, sealed, sealed

Я вспомнил про чудесные sealed и еще раз, кстати, sealed классы, в которых все наследники известны на этапе компиляции, а значит, с этим можно что-то сделать.

Рефлексия

Reflection (рефлексия) в программировании — это возможность программы анализировать свою структуру и поведение во время выполнения. В Kotlin, языке программирования, совместимом с Java, рефлексия является мощным инструментом, позволяющим программистам получать информацию о классах, функциях, переменных и методах во время выполнения программы.
Однако, следует быть осторожным, используя рефлексию. Она может снизить производительность и привести к ошибкам времени выполнения, так как не гарантируется, что все операции будут безопасными и корректными.
Источник: https://kolesnikovdev.ru/refleksiya-v-kotlin-prostye-primery-i-ispol

Вернемся к предыдущей части про sealed: самое частое использование, которое я видел в коде — ветвление без дефолта — if без else, что позволяет работать с экземпляром, зная, что все возможные варианты учтены, например, логирование сетевых ошибок.

В контексте sealed классов рефлексия позволяет узнать список наследников — это ключевой момент данной статьи.

Решение проблемы

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

Вместо обычного объявления модуля >>

val someModule = module {}

>> Я сделал базовый интерфейс модуля, sealed интерфейс и его реализацию следующим образом:

import org.koin.core.module.Module

interface KoinModule {
    val module: Module
}

sealed interface DataModule : KoinModule

class SomeModule : DataModule {
    override val module: Module = module {}
}

KoinModule — интерфейс для удобной реализации методов

DataModule — интерфейс из :data модуля моего проекта.

SomeModule — класс с реализацией зависимостей для Koin.

Далее вместо обычной декларации модулей >>

startKoin {
    modules(someModule)
}

>> «Пройдемся по всем наследникам и вытащим из них модули»

import kotlin.reflect.KClass
import org.koin.core.module.Module

fun  KClass.collectModules(): List =
    sealedSubclasses.map {
        it.constructors.first().call().module
    }
    
val dataModules = DataModule::class.collectModules()

С рефлексией с помощью sealedSubclasses получим список всех наследников DataModule::class, взяв из каждого нужный нам module: среди конструкторов возьмем первый (он же и единственный), вызовем его, и map по нужному полю. В дженерике использую KoinModule, чтобы задать единый метод для всех модулей проекта.

И, собственно, декларация:

startKoin {
    androidContext(this@App)
    modules(dataModules)
}

Было:

06780974ff5b25a0b855db84a10ddbe7.png

Стало:

fa5378ef2428f229cec351de3e7e3cde.png

На мой взгляд, всё это классно, но как говорит один очень хороший человек по имени Н.: «Главное не забывать 3 пункт sudo)»

No errors, no warnings, gentlemen and ladies!

© Habrahabr.ru