Saltar al contenido
K
Cimientos del código compartidoPaso a paso30 min de lectura · 7 min de máquina Intermedio

Arrancar Koin desde Android y desde iOS

Al terminar: Arrancar el grafo de Koin desde Android y desde iOS y pedir el ViewModel en cada interfaz.

El grafo de Koin: un solo grafo en commonMain está escrito y comprobado, pero ninguna app lo arranca todavía. Esta lección conecta las dos: Android lo arranca desde su Application y pide el ViewModel con koinViewModel(); iOS lo arranca desde su App y la fábrica de iosMain le pide el ViewModel a Koin. Swift no llegará a saber qué es Koin: solo llamará a una función de arranque.

Al terminar, las dos apps funcionan igual que antes, pero el ViewModel ya no lo construye cada pantalla con su propio Pokedex: lo construye el grafo.

Paso 1 · Arrancarlo desde Android

gradle
libs.versions.tomlse modifica: [libraries]
androidApp
build.gradle.ktsse modifica: dependencies
src/main/kotlin/.../android/PokedexApplication.ktse crea
src/main/kotlin/.../android/home/HomeScreen.ktse modifica
src/main/AndroidManifest.xmlse modifica
Lo que se toca en `androidApp`, y las librerías que se instalan.

Instala las dos librerías de Koin para androidApp. Es la receta corta de siempre. koin-android ya está en el catálogo (la instalaste en shared); falta koin-compose-viewmodel, que trae koinViewModel(), la función de Compose que le pide a Koin el ViewModel de la pantalla. En gradle/libs.versions.toml, al final de [libraries]:

libs.versions.toml: la línea nueva de [libraries]
# koinViewModel(): pide a Koin el ViewModel desde un composable.
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }

Y en androidApp/build.gradle.kts, dentro de dependencies { … }, justo debajo de implementation(projects.shared):

build.gradle.kts de androidApp: dependencias nuevas
    // androidContext(): lo usa PokedexApplication, que vive en androidApp. No
    // basta con que shared la tenga: implementation no se transmite entre
    // módulos. Y aunque hoy la traiga por dentro koin-compose-viewmodel, quien
    // la usa la declara.
    implementation(libs.koin.android)
    // koinViewModel(): lo usa la pantalla de Android.
    implementation(libs.koin.compose.viewmodel)

Pulsa Sync Now. Qué pasa si te saltas alguna, lo verás en los ficheros siguientes:

  • Sin koin.compose.viewmodel, HomeScreen.kt no compila: Unresolved reference 'compose' en el import y Unresolved reference 'koinViewModel'.
  • Sin koin.android, hoy compila igual, porque koin-compose-viewmodel la trae por dentro. Se declara de todos modos porque PokedexApplication la usa directamente: no dependas de lo que otra librería arrastre.

El archivo. PokedexApplication.kt es la clase que Android crea una vez, al arrancar el proceso, antes que cualquier pantalla: el sitio de arrancar Koin.

Créalo con clic derecho sobre la carpeta androidApp → New → File, con la ruta desde esa carpeta, src/main/kotlin/com/sebastiannarvaez/pokedex/android/PokedexApplication.kt. Escríbelo:

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/PokedexApplication.kt
package com.sebastiannarvaez.pokedex.android
 
import android.app.Application
import com.sebastiannarvaez.pokedex.di.initKoin
import org.koin.android.ext.koin.androidContext
 
/**
 * Arranca el grafo una sola vez, al crearse el proceso.
 *
 * `androidContext` deja el Context dentro de Koin, que es como las
 * dependencias que lo necesitan —la base de datos, mas adelante— lo reciben
 * sin que nadie lo pase a mano de constructor en constructor.
 */
class PokedexApplication : Application() {
 
    override fun onCreate() {
        super.onCreate()
        initKoin { androidContext(this@PokedexApplication) }
    }
}

onCreate es la función que Android llama una vez, cuando el componente se crea (El ciclo de vida de la Activity). Aquí se arranca Koin con el Context dentro. Y this@PokedexApplication es «esta PokedexApplication»: dentro de las llaves de initKoin { … }, this a secas ya es el KoinApplication de Koin (una lambda con receptor, como la de la lección anterior), y la @ dice de cuál de los dos hablas.

Decláralo en el manifiesto. Android solo crea la Application si el manifiesto la nombra. Abre androidApp/src/main/AndroidManifest.xml; en la etiqueta <application, añade como primer atributo:

AndroidManifest.xml: la línea nueva, dentro de <application
        android:name=".PokedexApplication"

El punto inicial significa «relativo al namespace de androidApp»: Android lo completa con él. Con el de esta guía (com.sebastiannarvaez.pokedex.android) resulta com.sebastiannarvaez.pokedex.android.PokedexApplication, que es donde acabas de crear la clase. Si tu namespace es otro (míralo en androidApp/build.gradle.kts), la clase debe estar en ese paquete y su línea package también. Sin esa línea en el manifiesto, initKoin no se llama nunca y la app falla al pedir el ViewModel.

La pantalla pide su ViewModel al grafo. Abre HomeScreen.kt (androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/home/HomeScreen.kt). Cambia dos cosas: los imports, para dejar de construir un Pokedex() a mano, y el parámetro por defecto:

HomeScreen.kt: imports, antes
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sebastiannarvaez.pokedex.Pokedex
HomeScreen.kt: imports, después
import org.koin.compose.viewmodel.koinViewModel
HomeScreen.kt: la firma, antes y después
// antes
fun HomeScreen(modifier: Modifier = Modifier, viewModel: HomeViewModel = viewModel { HomeViewModel(Pokedex()) }) {
// después: Koin construye el ViewModel con el grafo de Modules.kt
fun HomeScreen(modifier: Modifier = Modifier, viewModel: HomeViewModel = koinViewModel()) {

Ejecuta la app de Android (Ejecutar en Android): enseña el saludo y el latido avanzando, igual que antes, pero el ViewModel viene del grafo.

Paso 2 · Arrancarlo desde iOS

shared
src/iosMain/kotlin/.../di/KoinIos.ktse crea
src/iosMain/kotlin/.../feature/home/HomeIos.ktse modifica
iosApp (en Xcode: Project navigator)
iosApp/iOSApp.swiftse modifica
iosApp/ContentView.swiftse modifica
Un fichero nuevo de Kotlin, y tres cambios en lo que ya existe.

El archivo. Swift no puede llamar a initKoin con comodidad, y la razón es una trampa de nombres. Primero el fichero y luego el porqué.

Créalo en Android Studio con clic derecho sobre la carpeta shared → New → File, con la ruta desde esa carpeta:

src/iosMain/kotlin/com/sebastiannarvaez/pokedex/di/KoinIos.kt

Escríbelo:

shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/di/KoinIos.kt
package com.sebastiannarvaez.pokedex.di
 
/**
 * El arranque de Koin para iOS.
 *
 * Swift no importa ni configura Koin: solo llama a esta función, una vez, al
 * abrirse la app. Así la interfaz de iOS no depende de la librería de
 * inyección que use el núcleo. Es la excepción controlada a «sin funciones de
 * nivel superior»: una sola función de arranque, sin lógica.
 *
 * Ojo con el nombre al otro lado: `init` seguido de mayúscula es un prefijo
 * reservado de Objective-C, así que `initKoin` llegaría a Swift como
 * `doInitKoin`. Por eso esta función se llama distinto.
 */
fun startKoinIos() {
    initKoin()
}

Cuando algo queda feo desde Swift, la solución suele ser una función de conveniencia en Kotlin, no un envoltorio en Swift. Nombrarla bien de este lado ahorra explicar un nombre raro del otro.

La fábrica de iOS pide el ViewModel al grafo. En shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/feature/home/HomeIos.kt (el de la lección El mismo ViewModel en iOS), ya no hace falta recibir un Pokedex por parámetro. Cambia el import, la firma y la línea de la fábrica (si queda el import com.sebastiannarvaez.pokedex.Pokedex, bórralo: ya no se usa):

HomeIos.kt: antes
import com.sebastiannarvaez.pokedex.Pokedex
// …
fun homeViewModel(owner: ViewModelStoreOwner, pokedex: Pokedex): HomeViewModel =
    ViewModelProvider.create(
        owner = owner,
        factory = viewModelFactory { initializer { HomeViewModel(pokedex) } },
    )[HomeViewModel::class]
HomeIos.kt: después
import org.koin.mp.KoinPlatform
// …
fun homeViewModel(owner: ViewModelStoreOwner): HomeViewModel =
    ViewModelProvider.create(
        owner = owner,
        // Koin lo construye, el almacén lo entierra: cada uno hace lo suyo.
        factory = viewModelFactory { initializer { KoinPlatform.getKoin().get<HomeViewModel>() } },
    )[HomeViewModel::class]

KoinPlatform.getKoin() es el Koin que arrancó startKoinIos: la fábrica le pide el HomeViewModel y el almacén del dueño lo entierra, cada uno hace lo suyo.

Ahora, en Xcode:

Una struct puede tener un init(), el código que corre justo al crearla (en struct frente a class viste los inicializadores). El de la App corre una vez, al abrirse la app y antes de la primera pantalla: es el sitio de arrancar Koin, y es el mismo que usa la documentación de Koin.

Abre iOSApp.swift (Project navigator → iosApp) y sustituye todo su contenido por esto; es la misma App con un init y el import Shared:

iosApp/iosApp/iOSApp.swift
import SwiftUI
import Shared
 
@main
struct iOSApp: App {
 
    init() {
        // Una sola vez, al abrirse la app. Swift no configura Koin: solo hace
        // esta llamada, que el módulo compartido expone a propósito.
        KoinIosKt.startKoinIos()
    }
 
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Y en ContentView.swift, la vista deja de construir un Pokedex(). Borra la línea private let pokedex = Pokedex() y cambia la que crea el ViewModel:

ContentView.swift: la línea que cambia, antes y después
// antes
let viewModel = HomeIosKt.homeViewModel(owner: owner, pokedex: pokedex)
// después: la fábrica ya no necesita nada más que el dueño
let viewModel = HomeIosKt.homeViewModel(owner: owner)

Compila (Cmd+B) y ejecuta (Cmd+R). El simulador enseña el saludo y el latido, y el ViewModel viene del grafo.

Punto de control

  • koin-android y koin-compose-viewmodel están en las dependencias de androidApp, y el catálogo tiene la entrada koin-compose-viewmodel.
  • Existe PokedexApplication.kt, y el manifiesto (androidApp/src/main/AndroidManifest.xml) declara android:name=".PokedexApplication". Sin esa línea, initKoin no se llama nunca.
  • HomeScreen.kt pide su ViewModel con koinViewModel().
  • Existe KoinIos.kt en iosMain con startKoinIos(); iOSApp.swift la llama en su init, y esa es la única referencia a Koin en todo el Swift.
  • HomeIos.kt pide el HomeViewModel a KoinPlatform.getKoin(), y ContentView.swift llama a homeViewModel(owner:) sin Pokedex.
  • ./gradlew :shared:jvmTest sigue pasando los mismos tests que al acabar la lección anterior, entre ellos elGrafoSePuedeConstruirEntero: 10 con tus ejercicios (6 sin ellos). En Android y el simulador, 9 (5 sin ejercicios).
  • Las dos apps enseñan «Hola desde …» con el latido avanzando, y el ViewModel viene del grafo en las dos.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.¿Por qué la función de arranque para iOS se llama startKoinIos() y no initKoin()?

  2. 2.HomeScreen.kt usa koinViewModel(). ¿Qué librería tiene que estar en androidApp, y qué pasa si falta?

  3. 3.¿Por qué arranca Koin PokedexApplication y no MainActivity?

  4. 4.¿Qué hace KoinPlatform.getKoin() en HomeIos.kt?

Faltan 4 de 4