Konfigurowanie ViewModel w KMP

ViewModel z AndroidaX pełni funkcję pomostu, ustanawiając jasną umowę między wspólną logiką biznesową a komponentami interfejsu. Ten wzorzec pomaga zapewnić spójność danych na różnych platformach, a jednocześnie umożliwia dostosowywanie interfejsów do odmiennego wyglądu każdej platformy. Możesz dalej tworzyć interfejs użytkownika za pomocą Jetpack Compose na Androidzie i SwiftUI na iOS.

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

Konfigurowanie zależności

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

[versions]
androidx-viewmodel = 2.10.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ż ta zależność zostanie wyeksportowana do struktury binarnej:

// 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 poziomu Swift

Domyślnie żadna biblioteka dodana do bazy kodu nie jest automatycznie eksportowana do struktury binarnej. Jeśli interfejsy API nie są eksportowane, są dostępne w ramach binarnych tylko wtedy, gdy używasz ich w kodzie współdzielonym (z zbioru źródeł iosMain lub commonMain). W takim przypadku interfejsy API zawierałyby prefiks pakietu, np. klasa ViewModel byłaby dostępna jako klasa Lifecycle_viewmodelViewModel. Więcej informacji o eksportowaniu zależności znajdziesz w artykule Eksportowanie zależności do plików binarnych.

Aby poprawić komfort użytkowania, możesz wyeksportować zależność ViewModel do struktury binarnej za pomocą konfiguracji export w pliku build.gradle.kts, w którym definiujesz binarną strukturę 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 korutyn w obiekcie ViewModel właściwość viewModelScope jest powiązana z obiektem Dispatchers.Main.immediate, który może być domyślnie niedostępny na komputerze. Aby dział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 Dispatchers.Maindokumentacji.

Używanie ViewModel z commonMain lub androidMain

Nie ma konkretnych wymagań dotyczących używania klasy ViewModel w commonMainshared ani w androidMainsourceSet. Jedynym ograniczeniem jest to, że nie możesz używać żadnych interfejsów API specyficznych dla platformy i musisz je wyodrębnić. Jeśli na przykład używasz AndroidaApplication jako parametru konstruktora ViewModel, musisz zrezygnować z tego interfejsu API, tworząc jego abstrakcję.

Więcej informacji o tym, jak używać kodu specyficznego dla platformy, znajdziesz w artykule Kod specyficzny dla 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 w SwiftUI

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

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

Tworzenie funkcji, która pomoże w przypadku typów generycznych

Tworzenie instancji ogólnej klasy ViewModel wykorzystuje funkcję odbicia odwołania do klasy na Androidzie. Ponieważ typy generyczne w Objective-C nie obsługują wszystkich funkcji języków Kotlin ani Swift, nie możesz bezpośrednio pobrać z języka Swift obiektu ViewModel typu generycznego.

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, aby pobrać klasę ViewModel do utworzenia instancji:

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

Gdy zechcesz wywołać funkcję z poziomu Swift, możesz napisać funkcję ogólną typu T : ViewModel i użyć T.self, która może przekazać ObjCClass do funkcji resolveViewModel.

Łączenie zakresu ViewModel z cyklem życia SwiftUI

Następnym krokiem jest utworzenie IosViewModelStoreOwner, które implementuje interfejsy (protokoły) ObservableObjectViewModelStoreOwner. Powodem użycia 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 w Androidzie. Cykl życia tych obiektów ViewModel jest czyszczony, gdy ekran korzystający z IosViewModelStoreOwner zostaje odinstalowany 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

Ponieważ Hilt nie jest dostępny w projektach Kotlin Multiplatform, nie możesz bezpośrednio używać elementów ViewModel z adnotacją @HiltViewModelcommonMain sourceSet. W takim przypadku musisz użyć alternatywnego frameworka DI, np. Koin, kotlin-inject, Metro lub Kodein. Wszystkie platformy DI, które działają z Kotlin Multiplatform, znajdziesz na stronie klibs.io.

Obserwowanie przepływów w SwiftUI

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