Configura ViewModel para KMP

El ViewModel de AndroidX sirve como puente y establece un contrato claro entre tu lógica empresarial compartida y tus componentes de la IU. Este patrón ayuda a garantizar la coherencia de los datos en todas las plataformas y, al mismo tiempo, permite personalizar las IU para la apariencia distintiva de cada plataforma. Puedes seguir desarrollando tu IU con Jetpack Compose en Android y SwiftUI en iOS.

Obtén más información sobre los beneficios de usar ViewModel y todas las funciones en la documentación principal de ViewModel.

Configura dependencias

Para configurar el ViewModel de KMP en tu proyecto, define la dependencia en el archivo libs.versions.toml:

[versions]
androidx-viewmodel = 2.11.0

[libraries]
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }

Luego, agrega el artefacto al archivo build.gradle.kts de tu módulo de KMP y declara la dependencia como api, ya que se exportará al framework binario:

// You need the "api" dependency declaration here if you want better access to the classes from Swift code.
commonMain.dependencies {
  api(libs.androidx.lifecycle.viewmodel)
}

Exporta las APIs de ViewModel para acceder desde Swift

De forma predeterminada, cualquier biblioteca que agregues a tu base de código no se exportará automáticamente al framework binario. Si las APIs no se exportan, solo estarán disponibles en el framework binario si las usas en el código compartido (desde el conjunto de orígenes iosMain o commonMain). En ese caso, las APIs contendrían el prefijo del paquete, por ejemplo, una clase ViewModel estaría disponible como clase Lifecycle_viewmodelViewModel. Consulta Exporta dependencias a objetos binarios para obtener más información sobre la exportación de dependencias.

Para mejorar la experiencia, puedes exportar la dependencia de ViewModel al framework binario con la configuración de export en el archivo build.gradle.kts en el que defines el framework binario de iOS, lo que hace que se pueda acceder a las APIs de ViewModel directamente desde el código de Swift, al igual que desde el código de Kotlin:

listOf(
  iosX64(),
  iosArm64(),
  iosSimulatorArm64(),
).forEach {
  it.binaries.framework {
    // Add this line to all the targets you want to export this dependency
    export(libs.androidx.lifecycle.viewmodel)
    baseName = "shared"
  }
}

(Opcional) Usa viewModelScope en JVM para computadoras

Cuando se ejecutan corrutinas en un ViewModel, la propiedad viewModelScope está vinculada a Dispatchers.Main.immediate, que podría no estar disponible en computadoras de forma predeterminada. Para que funcione correctamente, agrega la dependencia kotlinx-coroutines-swing a tu proyecto:

// Optional if you use JVM Desktop
desktopMain.dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:[KotlinX Coroutines version]")
}

Consulta la documentación de Dispatchers.Main para obtener más detalles.

Usa ViewModel de commonMain o androidMain

No hay un requisito específico para usar la clase ViewModel en el commonMain compartido ni desde el sourceSet androidMain. La única consideración es que no puedes usar ninguna API específica de la plataforma y debes abstraerlas. Por ejemplo, si usas un Application de Android como parámetro del constructor de ViewModel, debes migrar de esta API abstrayéndola.

Puedes obtener más información para usar código específico de la plataforma en Código específico de la plataforma en Kotlin multiplataforma.

Por ejemplo, en el siguiente fragmento, se muestra una clase ViewModel con su fábrica, definida en commonMain:

// commonMain/MainViewModel.kt

class MainViewModel(
    private val repository: DataRepository,
) : ViewModel() { /* some logic */ }

// ViewModelFactory that retrieves the data repository for your app.
val mainViewModelFactory = viewModelFactory {
    initializer {
        MainViewModel(repository = getDataRepository())
    }
}

fun getDataRepository(): DataRepository = DataRepository()

Luego, en el código de la IU, puedes recuperar el ViewModel como de costumbre:

// androidApp/ui/MainScreen.kt

@Composable
fun MainScreen(
    viewModel: MainViewModel = viewModel(
        factory = mainViewModelFactory,
    ),
) {
// observe the viewModel state
}

Usa ViewModel desde SwiftUI

En Android, el ciclo de vida de ViewModel se controla automáticamente y se limita a un ComponentActivity, Fragment, NavBackStackEntry (Navigation 2) o rememberViewModelStoreNavEntryDecorator (Navigation 3). Sin embargo, SwiftUI en iOS no tiene un equivalente integrado para el ViewModel de AndroidX.

Para compartir el ViewModel con tu app de SwiftUI, debes agregar un código de configuración.

Crea una función para ayudar con los genéricos

Para crear una instancia de ViewModel genérica, se usa una función de reflexión de referencia de clase en Android. Debido a que los genéricos de Objective-C no admiten todas las funciones de Kotlin ni Swift, no puedes recuperar directamente un ViewModel de un tipo genérico de Swift.

Para solucionar este problema, puedes crear una función auxiliar que use ObjCClass en lugar del tipo de genéricos y, luego, usar getOriginalKotlinClass para recuperar la clase ViewModel de la que se creará una instancia:

// iosMain/ViewModelResolver.ios.kt

/**
 *   This function allows retrieving any ViewModel from Swift Code with generics. We only get
 *   [ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code
 *   doesn't preserve the generic class, but we can retrieve the original KClass in Kotlin.
 */
@BetaInteropApi
@Throws(IllegalArgumentException::class)
fun ViewModelStore.resolveViewModel(
    modelClass: ObjCClass,
    factory: ViewModelProvider.Factory,
    key: String?,
    extras: CreationExtras? = null,
): ViewModel {
    @Suppress("UNCHECKED_CAST")
    val vmClass = getOriginalKotlinClass(modelClass) as? KClass<ViewModel>
    require(vmClass != null) { "The modelClass parameter must be a ViewModel type." }

    val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty)
    return key?.let { provider[key, vmClass] } ?: provider[vmClass]
}

Luego, cuando quieras llamar a la función desde Swift, puedes escribir una función genérica de tipo T : ViewModel y usar T.self, que puede pasar el ObjCClass a la función resolveViewModel.

Conecta el alcance de ViewModel al ciclo de vida de SwiftUI

El siguiente paso es crear un IosViewModelStoreOwner que implemente las interfaces (protocolos) ObservableObject y ViewModelStoreOwner. El motivo de ObservableObject es poder usar esta clase como @StateObject en el código de SwiftUI:

// iosApp/IosViewModelStoreOwner.swift

class IosViewModelStoreOwner: ObservableObject, ViewModelStoreOwner {

    let viewModelStore = ViewModelStore()

    /// This function allows retrieving the androidx ViewModel from the store.
    /// It uses the utilify function to pass the generic type T to shared code
    func viewModel<T: ViewModel>(
        key: String? = nil,
        factory: ViewModelProviderFactory,
        extras: CreationExtras? = nil
    ) -> T {
        do {
            return try viewModelStore.resolveViewModel(
                modelClass: T.self,
                factory: factory,
                key: key,
                extras: extras
            ) as! T
        } catch {
            fatalError("Failed to create ViewModel of type \(T.self)")
        }
    }

    /// This is called when this class is used as a `@StateObject`
    deinit {
        viewModelStore.clear()
    }
}

Este propietario permite recuperar varios tipos de ViewModel, de manera similar a como se hace en Android. El ciclo de vida de esos ViewModels se borra cuando la pantalla que usa el IosViewModelStoreOwner se desinicializa y llama a deinit. Puedes obtener más información sobre la desinicialización en la documentación oficial.

En este punto, puedes crear una instancia de IosViewModelStoreOwner como un @StateObject en una vista de SwiftUI y llamar a la función viewModel para recuperar un ViewModel:

// iosApp/ContentView.swift

struct ContentView: View {

    /// Use the store owner as a StateObject to allow retrieving ViewModels and scoping it to this screen.
    @StateObject private var viewModelStoreOwner = IosViewModelStoreOwner()

    var body: some View {
        /// Retrieves the `MainViewModel` instance using the `viewModelStoreOwner`.
        /// The `MainViewModel.Factory` and `creationExtras` are provided to enable dependency injection
        /// and proper initialization of the ViewModel with its required `AppContainer`.
        let mainViewModel: MainViewModel = viewModelStoreOwner.viewModel(
            factory: MainViewModelKt.mainViewModelFactory
        )
        // ...
        // .. the rest of the SwiftUI code
    }
}

No disponible en Kotlin Multiplatform

Algunas de las APIs que están disponibles en Android no están disponibles en Kotlin Multiplatform.

Integración con Hilt

Debido a que Hilt no está disponible para proyectos de Kotlin Multiplatform, no puedes usar ViewModels directamente con la anotación @HiltViewModel en el sourceSet de commonMain. En ese caso, debes usar algún framework de DI alternativo, por ejemplo, Koin, kotlin-inject, Metro o Kodein. Puedes encontrar todos los frameworks de DI que funcionan con Kotlin Multiplatform en klibs.io.

Observa flujos en SwiftUI

No se admite directamente la observación de flujos de corrutinas en SwiftUI. Sin embargo, puedes usar la biblioteca KMP-NativeCoroutines o SKIE para permitir esta función.