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

A
Antonio Leiva
3 min lectura

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()  
)
Recomendado para ti