Konfigurowanie ViewModel w KMP

AndroidX ViewModel pełni funkcję pomostu, który ustanawia wyraźną umowę między współdzieloną logiką biznesową a komponentami interfejsu. Ten wzorzec pomaga zapewnić spójność danych na różnych platformach, umożliwiając jednocześnie dostosowywanie interfejsów do wyglądu poszczególnych platform. Możesz dalej tworzyć interfejs za pomocą Jetpack Compose na Androidzie i SwiftUI na iOS.

Więcej informacji o zaletach korzystania z ViewModel i wszystkich funkcjach znajdziesz w głównej dokumentacji ViewModel.

Konfigurowanie zależności

Aby skonfigurować KMP ViewModel w projekcie, zdefiniuj zależność w pliku libs.versions.toml:

[versions]
androidx-viewmodel = 2.11.0

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

Następnie dodaj artefakt do pliku build.gradle.kts modułu KMP i zadeklaruj zależność jako api, ponieważ zostanie ona wyeksportowana do binarnego frameworka:

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

Eksportowanie interfejsów API ViewModel na potrzeby dostępu z języka Swift

Domyślnie żadna biblioteka dodana do bazy kodu nie zostanie automatycznie wyeksportowana do platformy binarnej. Jeśli interfejsy API nie są eksportowane, są dostępne z binarnego frameworka tylko wtedy, gdy używasz ich we współdzielonym kodzie (ze zbioru źródeł iosMain lub commonMain). W takim przypadku interfejsy API będą zawierać prefiks pakietu, na przykład klasa ViewModel będzie dostępna jako klasa Lifecycle_viewmodelViewModel. Więcej informacji o eksportowaniu zależności znajdziesz w artykule o eksportowaniu zależności do plików binarnych.

Aby poprawić działanie, możesz wyeksportować zależność ViewModel do platformy binarnej, używając konfiguracji export w pliku build.gradle.kts, w którym definiujesz platformę binarną iOS. Dzięki temu interfejsy API ViewModel będą dostępne bezpośrednio z kodu Swift, tak samo jak z kodu 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"
  }
}

(Opcjonalnie) Korzystanie z viewModelScope na komputerze z JVM

Podczas uruchamiania współprogramów w ViewModel właściwość viewModelScope jest powiązana z Dispatchers.Main.immediate, która może być domyślnie niedostępna na komputerze. Aby działała prawidłowo, dodaj do projektu zależność kotlinx-coroutines-swing:

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

Więcej informacji znajdziesz w dokumentacji Dispatchers.Main.

Użyj ViewModel z commonMain lub androidMain

Nie ma konkretnych wymagań dotyczących używania klasy ViewModel w udostępnionym zbiorze commonMain ani w zbiorze źródłowym androidMain. Jedyną kwestią jest to, że nie można używać interfejsów API specyficznych dla platformy i trzeba je abstrakcyjnie przedstawić. Jeśli na przykład używasz interfejsu Android Application jako parametru konstruktora ViewModel, musisz zrezygnować z tego interfejsu API, tworząc jego abstrakcję.

Więcej informacji o używaniu kodu dla konkretnej platformy znajdziesz w artykule o kodzie dla konkretnej platformy w Kotlin Multiplatform.

Na przykład w tym fragmencie kodu znajduje się klasa ViewModel z fabryką zdefiniowaną w 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()

Następnie w kodzie interfejsu możesz pobrać ViewModel w zwykły sposób:

// androidApp/ui/MainScreen.kt

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

Używanie ViewModel z SwiftUI

Na Androidzie cykl życia ViewModel jest automatycznie obsługiwany i ograniczony do ComponentActivity, Fragment, NavBackStackEntry (Nawigacja 2) lub rememberViewModelStoreNavEntryDecorator (Nawigacja 3). SwiftUI na iOS nie ma jednak wbudowanego odpowiednika AndroidX ViewModel.

Aby udostępnić ViewModel aplikacji SwiftUI, musisz dodać kod konfiguracji.

Tworzenie funkcji ułatwiającej korzystanie z typów ogólnych

Tworzenie instancji ogólnego modelu ViewModel korzysta z funkcji refleksji odwołania do klasy na Androidzie. Ponieważ typy ogólne Objective-C nie obsługują wszystkich funkcji Kotlina ani Swifta, nie możesz bezpośrednio pobrać ViewModel typu ogólnego ze Swifta.

Aby rozwiązać ten problem, możesz utworzyć funkcję pomocniczą, która będzie używać ObjCClass zamiast typu ogólnego, a następnie użyć getOriginalKotlinClass do pobrania klasy ViewModel, aby utworzyć instancję:

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

Następnie, gdy chcesz wywołać funkcję ze Swift, możesz napisać funkcję ogólną typu T : ViewModel i użyć T.self, która może przekazać ObjCClass do funkcji resolveViewModel.

Połączenie zakresu ViewModel z cyklem życia SwiftUI

Następnym krokiem jest utworzenie klasy IosViewModelStoreOwner, która implementuje interfejsy (protokoły) ObservableObject i ViewModelStoreOwner. Powodem, dla którego używamy ObservableObject, jest możliwość użycia tej klasy jako @StateObject w kodzie 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()
    }
}

Ten właściciel umożliwia pobieranie wielu typów ViewModel, podobnie jak na Androidzie. Cykl życia tych modeli ViewModels jest czyszczony, gdy ekran korzystający z IosViewModelStoreOwner jest deinicjalizowany i wywołuje deinit. Więcej informacji o deinicjalizacji znajdziesz w oficjalnej dokumentacji.

W tym momencie możesz po prostu utworzyć instancję IosViewModelStoreOwner jako @StateObject w widoku SwiftUI i wywołać funkcję viewModel, aby pobrać 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
    }
}

Niedostępne w Kotlin Multiplatform

Niektóre interfejsy API dostępne na Androidzie nie są dostępne w Kotlin Multiplatform.

Integracja z Hilt

Hilt nie jest dostępny w przypadku projektów Kotlin Multiplatform, dlatego nie możesz bezpośrednio używać modeli widoku z adnotacją @HiltViewModel w zbiorze źródeł commonMain. W takim przypadku musisz użyć innej platformy DI, np. Koin, kotlin-inject, Metro lub Kodein. Wszystkie platformy DI, które współpracują z Kotlin Multiplatform, znajdziesz na stronie klibs.io.

Obserwowanie przepływów w SwiftUI

Obserwowanie przepływów współprogramów w SwiftUI nie jest bezpośrednio obsługiwane. Możesz jednak użyć biblioteki KMP-NativeCoroutines lub SKIE, aby zezwolić na tę funkcję.