How reflection in Kotlin helps automate working with Koin
When working on a large multi-module project, I often find myself in a situation where I forget to add a new module to startKoin
why I catch org.koin.core.error.NoDefinitionFoundException
– lack of declaration of the type that Koin is trying to inject, and therefore, since, in my opinion, the main concept of IT is the automation of our lives, it would be nice to automate this aspect as well.
if (Koin != Hilt) return
At first I tried to wrap a module so that when called, this module would be added to a certain list, which can then be used for declaration, but with more detailed delving into Koin, it turned out that it makes injections at runtime, and not at the compilation stage, like Hilt, and therefore, this option was unsuccessful.
sealed, sealed, sealed
I remembered the wonderful sealed
and again, by the way, sealed
classes in which all descendants are known at the compilation stage, which means something can be done about it.
Reflection
Reflection in programming is the ability of a program to analyze its structure and behavior during execution. In Kotlin, a programming language compatible with Java, reflection is a powerful tool that allows programmers to obtain information about classes, functions, variables, and methods during program execution.
However, you should be careful when using reflection. It can reduce performance and lead to runtime errors because it is not guaranteed that all operations will be safe and correct.
Source: https://kolesnikovdev.ru/refleksiya-v-kotlin-prostye-primery-i-ispol
Let's go back to the previous part about sealed
: The most common use I've seen in code is branching without defaulting – if
without else
which allows you to work with the instance knowing that all possible options are taken into account, for example, logging network errors.
In context sealed
class reflection allows you to find out the list of heirs – this is the key point of this article.
Solving the problem
My idea is not a panacea, but only one of the options, and perhaps in the comments someone will suggest a more effective and safe way.
Instead of the usual module declaration >>
val someModule = module {}
>> I made a basic module interface, a sealed interface and its implementation like this:
import org.koin.core.module.Module
interface KoinModule {
val module: Module
}
sealed interface DataModule : KoinModule
class SomeModule : DataModule {
override val module: Module = module {}
}
KoinModule
– interface for convenient implementation of methods
DataModule
– interface from :data
module of my project.
SomeModule
– a class with implementation of dependencies for Koin.
Next, instead of the usual module declaration >>
startKoin {
modules(someModule)
}
>> “Let's go through all the heirs and pull out the modules from them”
import kotlin.reflect.KClass
import org.koin.core.module.Module
fun <T : KoinModule> KClass<T>.collectModules(): List<Module> =
sealedSubclasses.map {
it.constructors.first().call().module
}
val dataModules = DataModule::class.collectModules()
With reflection using sealedSubclasses
get a list of all heirs DataModule::class
taking from each the one we need module
: among the constructors, let’s take the first one (it’s also the only one), call it, and map
by the required field. I use it in generics KoinModule
to set a single method for all project modules.
And, in fact, the declaration:
startKoin {
androidContext(this@App)
modules(dataModules)
}
Was:
Became:
In my opinion, all this is cool, but as one very good person named N. says: “The main thing is not to forget step 3 of sudo)”
No errors, no warnings, gentlemen and ladies!