Configurer ViewModel pour KMP

AndroidX ViewModel sert de pont, établissant un contrat clair entre votre logique métier partagée et vos composants d'UI. Ce modèle permet d'assurer la cohérence des données sur toutes les plates-formes, tout en permettant de personnaliser les UI pour l'apparence distincte de chaque plate-forme. Vous pouvez continuer à développer votre UI avec Jetpack Compose sur Android et SwiftUI sur iOS.

Pour en savoir plus sur les avantages de l'utilisation de ViewModel et sur toutes les fonctionnalités, consultez la documentation principale de ViewModel.

Configurer des dépendances

Pour configurer le ViewModel KMP dans votre projet, définissez la dépendance dans le fichier libs.versions.toml :

[versions]
androidx-viewmodel = 2.9.3

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

Ajoutez ensuite l'artefact au fichier build.gradle.kts de votre module KMP et déclarez la dépendance en tant que api, car cette dépendance sera exportée vers le framework binaire :

// 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)
}

Exporter les API ViewModel pour y accéder depuis Swift

Par défaut, toute bibliothèque que vous ajoutez à votre code n'est pas automatiquement exportée vers le framework binaire. Si les API ne sont pas exportées, elles ne sont disponibles à partir du framework binaire que si vous les utilisez dans le code partagé (à partir de l'ensemble de sources iosMain ou commonMain). Dans ce cas, les API contiennent le préfixe du package. Par exemple, une classe ViewModel est disponible en tant que classe Lifecycle_viewmodelViewModel. Pour en savoir plus sur l'exportation des dépendances, consultez la section Exporter les dépendances vers des binaires.

Pour améliorer l'expérience, vous pouvez exporter la dépendance ViewModel vers le framework binaire à l'aide de la configuration export dans le fichier build.gradle.kts où vous définissez le framework binaire iOS. Cela rend les API ViewModel accessibles directement à partir du code Swift, comme à partir du code 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"
  }
}

(Facultatif) Utiliser viewModelScope sur JVM Desktop

Lorsque vous exécutez des coroutines dans un ViewModel, la propriété viewModelScope est liée à Dispatchers.Main.immediate, qui peut ne pas être disponible sur ordinateur par défaut. Pour que cela fonctionne correctement, ajoutez la dépendance kotlinx-coroutines-swing à votre projet :

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

Pour en savoir plus, consultez la documentation Dispatchers.Main.

Utiliser ViewModel à partir de commonMain ou androidMain

Il n'existe aucune exigence spécifique concernant l'utilisation de la classe ViewModel dans le commonMain partagé, ni à partir du sourceSet androidMain. La seule chose à prendre en compte est que vous ne pouvez pas utiliser d'API spécifiques à une plate-forme et que vous devez les abstraire. Par exemple, si vous utilisez un Application Android comme paramètre de constructeur ViewModel, vous devez migrer depuis cette API en l'abstrayant.

Pour en savoir plus sur l'utilisation de code spécifique à une plate-forme, consultez Code spécifique à une plate-forme dans Kotlin Multiplatform.

Par exemple, l'extrait suivant présente une classe ViewModel avec sa fabrique, définie dans 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()

Ensuite, dans votre code d'UI, vous pouvez récupérer le ViewModel comme d'habitude :

// androidApp/ui/MainScreen.kt

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

Utiliser ViewModel depuis SwiftUI

Sur Android, le cycle de vie ViewModel est automatiquement géré et limité à un ComponentActivity, Fragment, NavBackStackEntry (Navigation 2) ou rememberViewModelStoreNavEntryDecorator (Navigation 3). Toutefois, SwiftUI sur iOS ne dispose pas d'équivalent intégré pour AndroidX ViewModel.

Pour partager le ViewModel avec votre application SwiftUI, vous devez ajouter du code de configuration.

Créer une fonction pour faciliter l'utilisation des génériques

L'instanciation d'une instance ViewModel générique utilise une fonctionnalité de réflexion de référence de classe sur Android. Étant donné que les génériques Objective-C ne sont pas compatibles avec toutes les fonctionnalités de Kotlin ou de Swift, vous ne pouvez pas récupérer directement un ViewModel d'un type générique à partir de Swift.

Pour résoudre ce problème, vous pouvez créer une fonction d'assistance qui utilisera ObjCClass au lieu du type générique, puis utiliser getOriginalKotlinClass pour récupérer la classe ViewModel à instancier :

// 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]
}

Ensuite, lorsque vous souhaitez appeler la fonction depuis Swift, vous pouvez écrire une fonction générique de type T : ViewModel et utiliser T.self, qui peut transmettre ObjCClass à la fonction resolveViewModel.

Connecter le champ d'application ViewModel au cycle de vie SwiftUI

L'étape suivante consiste à créer un IosViewModelStoreOwner qui implémente les interfaces (protocoles) ObservableObject et ViewModelStoreOwner. La raison de l'utilisation de ObservableObject est de pouvoir utiliser cette classe comme @StateObject dans le code 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()
    }
}

Ce propriétaire permet de récupérer plusieurs types de ViewModel, comme sur Android. Le cycle de vie de ces ViewModels est effacé lorsque l'écran utilisant IosViewModelStoreOwner est désinitialisé et appelle deinit. Pour en savoir plus sur la désinitialisation, consultez la documentation officielle.

À ce stade, vous pouvez simplement instancier IosViewModelStoreOwner en tant que @StateObject dans une vue SwiftUI et appeler la fonction viewModel pour récupérer 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
    }
}

Non disponible dans Kotlin Multiplatform

Certaines API disponibles sur Android ne le sont pas dans Kotlin Multiplatform.

Intégration à Hilt

Comme Hilt n'est pas disponible pour les projets Kotlin Multiplatform, vous ne pouvez pas utiliser directement les ViewModels avec l'annotation @HiltViewModel dans le sourceSet commonMain. Dans ce cas, vous devez utiliser un autre framework d'injection de dépendances, par exemple Koin, kotlin-inject, Metro ou Kodein. Vous trouverez tous les frameworks d'injection de dépendances fonctionnant avec Kotlin Multiplatform sur klibs.io.

Observer les flux dans SwiftUI

L'observation des flux de coroutines dans SwiftUI n'est pas directement prise en charge. Toutefois, vous pouvez utiliser la bibliothèque KMP-NativeCoroutines ou SKIE pour autoriser cette fonctionnalité.