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
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]:
# 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):
// 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.ktno compila:Unresolved reference 'compose'en elimportyUnresolved reference 'koinViewModel'. - Sin
koin.android, hoy compila igual, porquekoin-compose-viewmodella trae por dentro. Se declara de todos modos porquePokedexApplicationla 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:
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:
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:
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sebastiannarvaez.pokedex.Pokedeximport org.koin.compose.viewmodel.koinViewModel// 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
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.ktEscríbelo:
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):
import com.sebastiannarvaez.pokedex.Pokedex
// …
fun homeViewModel(owner: ViewModelStoreOwner, pokedex: Pokedex): HomeViewModel =
ViewModelProvider.create(
owner = owner,
factory = viewModelFactory { initializer { HomeViewModel(pokedex) } },
)[HomeViewModel::class]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:
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:
// 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-androidykoin-compose-viewmodelestán en las dependencias deandroidApp, y el catálogo tiene la entradakoin-compose-viewmodel.- Existe
PokedexApplication.kt, y el manifiesto (androidApp/src/main/AndroidManifest.xml) declaraandroid:name=".PokedexApplication". Sin esa línea,initKoinno se llama nunca. HomeScreen.ktpide su ViewModel conkoinViewModel().- Existe
KoinIos.kteniosMainconstartKoinIos();iOSApp.swiftla llama en suinit, y esa es la única referencia a Koin en todo el Swift. HomeIos.ktpide elHomeViewModelaKoinPlatform.getKoin(), yContentView.swiftllama ahomeViewModel(owner:)sinPokedex../gradlew :shared:jvmTestsigue pasando los mismos tests que al acabar la lección anterior, entre elloselGrafoSePuedeConstruirEntero: 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.¿Por qué la función de arranque para iOS se llama
startKoinIos()y noinitKoin()?2.
HomeScreen.ktusakoinViewModel(). ¿Qué librería tiene que estar enandroidApp, y qué pasa si falta?3.¿Por qué arranca Koin
PokedexApplicationy noMainActivity?4.¿Qué hace
KoinPlatform.getKoin()enHomeIos.kt?