Agregar el shared module como dependencia
// androidApp/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
dependencies {
// El shared module — se referencia como proyecto Gradle
implementation(project(":shared"))
// Dependencias Android-only (UI, ViewModel, etc.)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
// Compose, Navigation, etc.
}
// settings.gradle.kts (raíz) — ambos módulos declarados
include(":androidApp", ":shared")
Koin multiplatform — DI compartido y por plataforma
// shared/commonMain — módulo de DI compartido
// Define los bindings que son iguales en todas las plataformas
val sharedModule = module {
// Use cases — Kotlin puro, mismos en todas las plataformas
factory { GetProductosUseCase(get()) }
factory { BuscarProductosUseCase(get()) }
factory { SincronizarUseCase(get()) }
// Repositorios
single<ProductoRepository> { ProductoRepositoryImpl(get(), get()) }
// API client
single { ProductoApi(get()) }
single { crearHttpClient(get()) } // usa el engine que provee cada plataforma
}
// shared/androidMain — módulo Android-specific
val androidModule = module {
// Engine de Ktor para Android
single<HttpClientEngine> { OkHttp.create() }
// Implementación Android de Settings usando SharedPreferences
single { Settings(androidContext()) }
}
// androidApp — Application class donde se inicializa Koin
class MiApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MiApp)
modules(
sharedModule, // módulo compartido
androidModule, // módulo Android-specific
androidAppModule // módulo de la app Android (ViewModels, etc.)
)
}
}
}
// androidApp — módulo de DI de la app Android
val androidAppModule = module {
// ViewModels de Android (no son parte del shared)
viewModel { ProductosAndroidViewModel(get(), get(), get()) }
viewModel { DetalleAndroidViewModel(get()) }
}
ViewModel de Android consumiendo el shared module
// androidApp — el ViewModel de Android es un wrapper liviano sobre el shared
// Convierte los tipos del shared en tipos observables de Android
class ProductosAndroidViewModel(
private val getProductos: GetProductosUseCase,
private val sincronizar: SincronizarUseCase,
private val buscar: BuscarProductosUseCase
) : ViewModel() {
// Reexponer el Flow del use case como StateFlow de Android
val uiState: StateFlow<ProductosUiState> = getProductos()
.map { productos -> ProductosUiState(productos = productos) }
.catch { e -> emit(ProductosUiState(error = e.message)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ProductosUiState(isLoading = true)
)
fun onPullRefresh() {
viewModelScope.launch {
try {
sincronizar()
} catch (e: Exception) {
// manejar error
}
}
}
fun buscarProductos(query: String) {
viewModelScope.launch {
val resultado = buscar(query)
when (resultado) {
is Resultado.Exito -> { /* actualizar state */ }
is Resultado.Error -> { /* mostrar error */ }
Resultado.Cargando -> { /* mostrar loader */ }
}
}
}
}
// ProductosUiState vive en el shared module — se reutiliza en Android e iOS
Compose en Android con el shared module
// Compose en Android consume los datos del shared module exactamente igual
// que con cualquier otro ViewModel — no hay diferencia visible
@Composable
fun ProductosPantalla(
viewModel: ProductosAndroidViewModel = koinViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when {
uiState.isLoading -> CircularProgressIndicator()
uiState.error != null -> ErrorView(uiState.error!!)
else -> LazyColumn {
items(uiState.productos, key = { it.id }) { producto ->
// Producto es el data class del shared module — Kotlin puro
ProductoCard(producto = producto)
}
}
}
}
@Composable
fun ProductoCard(producto: Producto) { // Producto del shared module
Card {
Column(modifier = Modifier.padding(16.dp)) {
Text(producto.nombre, style = MaterialTheme.typography.titleMedium)
Text("$${producto.precio}", style = MaterialTheme.typography.bodyLarge)
producto.descripcion?.let { Text(it) }
}
}
}
Pasar el Context de Android al shared module
El shared module no puede tener el Context de Android como dependencia directa — eso rompería la compilación en iOS. El patrón correcto es una variable global inicializada una vez al arrancar la app:
// androidMain — variable global de contexto
// (el único lugar donde esto es aceptable en Android)
lateinit var appContext: android.content.Context
// androidApp — inicializar en Application.onCreate()
class MiApp : Application() {
override fun onCreate() {
super.onCreate()
appContext = this // antes de startKoin
startKoin { /* ... */ }
}
}
// androidMain — usar en las implementaciones actual
actual class Settings actual constructor(name: String) {
private val prefs = appContext.getSharedPreferences(name, Context.MODE_PRIVATE)
// ...
}
// iosMain — no hay equivalente, la clase Settings usa NSUserDefaults directamente
// y no necesita ningún contexto de aplicación