ViewModel für KMP einrichten

Das AndroidX-ViewModel dient als Brücke und stellt einen klaren Vertrag zwischen Ihrer gemeinsamen Geschäftslogik und Ihren UI-Komponenten her. Dieses Muster trägt dazu bei, die Datenkonsistenz über Plattformen hinweg zu gewährleisten, und ermöglicht gleichzeitig die Anpassung von Benutzeroberflächen an das jeweilige Erscheinungsbild der Plattform. Sie können Ihre Benutzeroberfläche weiterhin mit Jetpack Compose auf Android und SwiftUI auf iOS entwickeln.

Weitere Informationen zu den Vorteilen von ViewModel und allen Funktionen finden Sie in der primären Dokumentation für ViewModel.

Abhängigkeiten einrichten

Wenn Sie das KMP-ViewModel in Ihrem Projekt einrichten möchten, definieren Sie die Abhängigkeit in der Datei libs.versions.toml:

[versions]
androidx-viewmodel = 2.11.0

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

Fügen Sie das Artefakt dann der Datei build.gradle.kts für Ihr KMP-Modul hinzu und deklarieren Sie die Abhängigkeit als api, da diese Abhängigkeit in das binäre Framework exportiert wird:

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

ViewModel APIs für den Zugriff über Swift exportieren

Standardmäßig wird eine Bibliothek, die Sie Ihrer Codebasis hinzufügen, nicht automatisch in das binäre Framework exportiert. Wenn die APIs nicht exportiert werden, sind sie nur dann im binären Framework verfügbar, wenn Sie sie im freigegebenen Code (aus dem Quell-Set iosMain oder commonMain) verwenden. In diesem Fall enthalten die APIs das Paketpräfix. Beispielsweise wäre eine ViewModel-Klasse als Lifecycle_viewmodelViewModel-Klasse verfügbar. Weitere Informationen zum Exportieren von Abhängigkeiten finden Sie unter Abhängigkeiten in Binärdateien exportieren.

Um die Nutzung zu verbessern, können Sie die ViewModel-Abhängigkeit in das binäre Framework exportieren. Verwenden Sie dazu die export-Einrichtung in der build.gradle.kts-Datei, in der Sie das binäre iOS-Framework definieren. Dadurch sind die ViewModel APIs direkt über den Swift-Code zugänglich, genau wie über den Kotlin-Code:

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"
  }
}

(Optional) viewModelScope auf JVM Desktop verwenden

Wenn Sie Koroutinen in einem ViewModel ausführen, ist das Attribut viewModelScope an Dispatchers.Main.immediate gebunden, das auf Computern standardmäßig möglicherweise nicht verfügbar ist. Damit es richtig funktioniert, fügen Sie Ihrem Projekt die Abhängigkeit kotlinx-coroutines-swing hinzu:

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

Weitere Informationen finden Sie in der Dokumentation zu Dispatchers.Main.

ViewModel aus commonMain oder androidMain verwenden

Es gibt keine spezifische Anforderung für die Verwendung der ViewModel-Klasse im freigegebenen commonMain oder im androidMain-sourceSet. Sie müssen nur beachten, dass Sie keine plattformspezifischen APIs verwenden können und diese abstrahieren müssen. Wenn Sie beispielsweise ein Android-Application als ViewModel-Konstruktorparameter verwenden, müssen Sie von dieser API migrieren, indem Sie sie abstrahieren.

Weitere Informationen zur Verwendung von plattformspezifischem Code finden Sie unter Plattformspezifischer Code in Kotlin Multiplatform.

Das folgende Snippet zeigt beispielsweise eine ViewModel-Klasse mit ihrer Factory, die in commonMain definiert ist:

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

Anschließend können Sie das ViewModel wie gewohnt in Ihrem UI-Code abrufen:

// androidApp/ui/MainScreen.kt

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

ViewModel aus SwiftUI verwenden

Unter Android wird der ViewModel-Lebenszyklus automatisch verwaltet und auf ein ComponentActivity, Fragment, NavBackStackEntry (Navigation 2) oder rememberViewModelStoreNavEntryDecorator (Navigation 3) beschränkt. SwiftUI unter iOS hat jedoch kein integriertes Äquivalent für das AndroidX-ViewModel.

Damit das ViewModel mit Ihrer SwiftUI-Anwendung geteilt werden kann, müssen Sie Einrichtungscode hinzufügen.

Funktion zur Unterstützung von Generics erstellen

Beim Instanziieren einer generischen ViewModel-Instanz wird unter Android ein Reflektionsfeature für Klassenreferenzen verwendet. Da Objective-C-Generics nicht alle Funktionen von Kotlin oder Swift unterstützen, können Sie ein ViewModel eines generischen Typs nicht direkt aus Swift abrufen.

Um dieses Problem zu beheben, können Sie eine Hilfsfunktion erstellen, die ObjCClass anstelle des generischen Typs verwendet, und dann getOriginalKotlinClass verwenden, um die zu instanziierende ViewModel-Klasse abzurufen:

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

Wenn Sie die Funktion dann über Swift aufrufen möchten, können Sie eine generische Funktion vom Typ T : ViewModel schreiben und T.self verwenden, um ObjCClass an die Funktion resolveViewModel zu übergeben.

ViewModel-Bereich mit SwiftUI-Lebenszyklus verbinden

Im nächsten Schritt erstellen Sie eine IosViewModelStoreOwner, die die Schnittstellen (Protokolle) ObservableObject und ViewModelStoreOwner implementiert. Der Grund für ObservableObject ist, dass diese Klasse im SwiftUI-Code als @StateObject verwendet werden kann:

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

Dieser Inhaber ermöglicht das Abrufen mehrerer ViewModel-Typen, ähnlich wie unter Android. Der Lebenszyklus dieser ViewModels wird gelöscht, wenn der Bildschirm, der die IosViewModelStoreOwner verwendet, deinitialisiert wird und deinit aufruft. Weitere Informationen zur Deinitialisierung finden Sie in der offiziellen Dokumentation.

An dieser Stelle können Sie das IosViewModelStoreOwner einfach als @StateObject in einer SwiftUI-Ansicht instanziieren und die Funktion viewModel aufrufen, um ein ViewModel abzurufen:

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

Nicht verfügbar in Kotlin Multiplatform

Einige der APIs, die unter Android verfügbar sind, sind in Kotlin Multiplatform nicht verfügbar.

Einbindung in Hilt

Da Hilt für Kotlin Multiplatform-Projekte nicht verfügbar ist, können Sie ViewModels mit der Annotation @HiltViewModel nicht direkt im commonMain-SourceSet verwenden. In diesem Fall müssen Sie ein alternatives DI-Framework verwenden, z. B. Koin, kotlin-inject, Metro oder Kodein. Alle DI-Frameworks, die mit Kotlin Multiplatform funktionieren, finden Sie unter klibs.io.

Flows in SwiftUI beobachten

Das Beobachten von Coroutine-Flows in SwiftUI wird nicht direkt unterstützt. Sie können jedoch entweder die Bibliothek KMP-NativeCoroutines oder SKIE verwenden, um diese Funktion zu aktivieren.