Koin para Kotlin Multiplatform: La inyección de dependencias fácil

Koin está preparada para ser usada en Kotlin Multiplatform, aunque si estás usando Compose Multiplatform, hay algunas cosas que a día de hoy están en beta.
Configuración de dependencias
Necesitas añadir la versión de Koin, y la de Koin Compose. Esto es porque una de las librerías que vamos a usar aún no está en el BOM de Koin:
[versions]
koin = "3.6.0-Beta4"
koinCompose = "1.2.0-Beta4"
[libraries]
koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin" }
koin-android = { group = "io.insert-koin", name = "koin-android" }
koin-core = { group = "io.insert-koin", name = "koin-core" }
koin-compose = { group = "io.insert-koin", name = "koin-compose" }
koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel", version.ref = "koinCompose"}
La 3.6.0-Beta4
es la primera versión que incluye soporte para ViewModels en el sourceSet common
, de tal forma que podemos definir las dependencias una sola vez si estás usando los ViewModels de Android.
Para poder usar la función koinViewModel
en el código común de compose, necesitas añadir la dependencia koin-compose-viewmodel
, que parece que a día de hoy no está en el BOM.
Por eso hay que añadir tanto la dependencia como la verisón.
Luego en el build.gradle
de composeApp
haríamos:
kotlin {
...
sourceSets {
...
androidMain.dependencies {
...
implementation(libs.koin.android)
}
commonMain.dependencies {
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
}
}
}
Definición de dependencias
Defines las dependencias como habitualmente en Koin, creando una serie de módulos organizados como más te interese:
val appModule = module {
single(named("apiKey")) { BuildConfig.API_KEY }
}
val dataModule = module {
factoryOf(::MoviesRepository)
factoryOf(::MoviesService)
}
val viewModelsModule = module {
viewModelOf(::HomeViewModel)
viewModelOf(::DetailViewModel)
}
Hay muchas formas de proveer las dependencias nativas. Una forma sencilla es usar un módulo definido con expect
:
expect val nativeModule: Module
Y luego implementarlo en cada target. En Android:
actual val nativeModule = module {
single { getDatabaseBuilder(get()).build().moviesDao() }
}
En iOS:
actual val nativeModule = module {
single { getDatabaseBuilder().build().moviesDao() }
}
Inicialización
La inicialización debe hacerse en cada plataforma, porque por ejemplo a Android hay que pasarle el contexto.
Para ello, puede ser útil crearse una función de utilidad que defina el código común de inicialización:
fun initKoin(config: KoinAppDeclaration? = null) {
startKoin {
config?.invoke(this)
modules(appModule, dataModule, viewModelsModule, nativeModule)
}
}
La config
nos permite añadir configuración extra en cada plataforma. Por ejemplo, en Android, nos iríamos al Application
, y en el onCreate()
haríamos:
class MoviesApp : Application() {
override fun onCreate() {
super.onCreate()
initKoin {
androidLogger(Level.DEBUG)
androidContext(this@MoviesApp)
}
}
}
Aquí el config
lo usamos para inicializar el logger de Android (que pinte los errores en el Logcat), y pasarle el contexto.
Estas dos funciones existen gracias a que hemos añadido la dependencia de koin.android
en el target de Android.
Para iOS sería, lo podemos hacer en el MainViewController
, usando el argumento configure
, una función que se ejecuta antes de inicializar el contenido:
fun MainViewController() = ComposeUIViewController(
configure = { initKoin() }
) {
App()
}
Finalmente, le tenemos que decir a Compose qué contexto de Koin va a usar. Si no hacemos nada, usará el KoinContext
actual por defecto, pero nos mostrará un warning en los logs.
Es tan fácil como envolver nuestro código con la función KoinContext
:
@Composable
@Preview
fun App() {
KoinContext {
Navigation()
}
}
Uso
Ahora Koin se usa como lo usarías en Android. Para dependencias estándar puedes usar el delegado by inject()
, y para ViewModels la función koinViewModel()
:
@Composable
fun HomeScreen(
onMovieClick: (Movie) -> Unit,
vm: HomeViewModel = koinViewModel()
)
Simplifica el uso de Coroutines con Flows
Funciones de extensión en Kotlin