Настройка ViewModel для KMP

AndroidX ViewModel служит связующим звеном, устанавливая чёткое взаимодействие между общей бизнес-логикой и компонентами пользовательского интерфейса. Этот шаблон обеспечивает согласованность данных на разных платформах, позволяя настраивать пользовательский интерфейс под особенности каждой платформы. Вы можете продолжить разработку пользовательского интерфейса с помощью Jetpack Compose на Android и SwiftUI на iOS.

Подробнее о преимуществах использования ViewModel и всех возможностях читайте в основной документации по ViewModel .

Настройка зависимостей

Чтобы настроить KMP ViewModel в своем проекте, определите зависимость в файле libs.versions.toml :

[versions]
androidx-viewmodel = 2.9.3

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

Затем добавьте артефакт в файл build.gradle.kts для вашего модуля KMP и объявите зависимость как api , поскольку эта зависимость будет экспортирована в двоичный фреймворк:

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

Экспорт API ViewModel для доступа из Swift

По умолчанию любая библиотека, добавляемая в кодовую базу, не будет автоматически экспортироваться в двоичный фреймворк. Если API не экспортированы, они доступны из двоичного фреймворка только при использовании их в общем коде (из исходного набора iosMain или commonMain ). В этом случае API будут содержать префикс package, например, класс ViewModel будет доступен как класс Lifecycle_viewmodelViewModel . Подробнее об экспорте зависимостей в двоичные файлы см. в разделе «Зависимости экспорта».

Для улучшения опыта вы можете экспортировать зависимость ViewModel в двоичный фреймворк, используя настройку export в файле build.gradle.kts , где вы определяете двоичный фреймворк iOS, что делает API ViewModel доступными непосредственно из кода Swift так же, как и из кода 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"
  }
}

(Необязательно) Использование viewModelScope на JVM Desktop

При запуске сопрограмм в ViewModel свойство viewModelScope привязывается к Dispatchers.Main.immediate , которое может быть недоступно по умолчанию в десктопной версии. Для корректной работы добавьте в свой проект зависимость kotlinx-coroutines-swing :

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

Более подробную информацию см. в документации Dispatchers.Main .

Используйте ViewModel из commonMain или androidMain

Нет особых требований к использованию класса ViewModel в общем commonMain или в исходном наборе androidMain . Единственное замечание — невозможность использования платформенно-зависимых API, и их необходимо абстрагировать. Например, если вы используете Application Android в качестве параметра конструктора ViewModel, вам необходимо отказаться от этого API, абстрагировав его.

Более подробная информация об использовании платформенно-зависимого кода доступна в разделе Платформенно-зависимый код в Kotlin Multiplatform .

Например, в следующем фрагменте представлен класс ViewModel со своей фабрикой, определенной в 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()

Затем в коде пользовательского интерфейса вы можете извлечь ViewModel обычным способом:

// androidApp/ui/MainScreen.kt

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

Использовать ViewModel из SwiftUI

В Android жизненный цикл ViewModel автоматически обрабатывается и ограничивается ComponentActivity , Fragment , NavBackStackEntry (навигация 2) или rememberViewModelStoreNavEntryDecorator (навигация 3). Однако в SwiftUI для iOS нет встроенного аналога для AndroidX ViewModel.

Чтобы использовать ViewModel совместно с вашим приложением SwiftUI, вам необходимо добавить некоторый код настройки.

Создайте функцию, которая поможет с дженериками

Для создания экземпляра универсального ViewModel в Android используется функция отражения ссылки на класс . Поскольку универсальные типы Objective-C поддерживают не все функции Kotlin и Swift, невозможно напрямую получить ViewModel универсального типа из Swift.

Чтобы решить эту проблему, вы можете создать вспомогательную функцию, которая будет использовать ObjCClass вместо универсального типа, а затем использовать getOriginalKotlinClass для извлечения класса ViewModel для создания экземпляра:

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

Затем, когда вы захотите вызвать функцию из Swift, вы можете написать универсальную функцию типа T : ViewModel и использовать T.self , которая может передать ObjCClass в функцию resolveViewModel .

Подключите область ViewModel к жизненному циклу SwiftUI

Следующий шаг — создание класса IosViewModelStoreOwner , реализующего интерфейсы (протоколы) ObservableObject и ViewModelStoreOwner . ObservableObject нужен для возможности использования этого класса в качестве @StateObject в коде 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()
    }
}

Этот владелец позволяет получать несколько типов ViewModel, аналогично Android. Жизненный цикл этих ViewModel завершается, когда экран, использующий IosViewModelStoreOwner , деинициализируется и вызывает deinit . Подробнее о деинициализации можно узнать в официальной документации .

На этом этапе вы можете просто создать экземпляр IosViewModelStoreOwner как @StateObject в представлении SwiftUI и вызвать функцию viewModel для получения 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
    }
}

Недоступно в Kotlin Multiplatform

Некоторые API, доступные в Android, недоступны в Kotlin Multiplatform.

Интеграция с Hilt

Поскольку Hilt недоступен для проектов Kotlin Multiplatform, вы не можете напрямую использовать ViewModel с аннотацией @HiltViewModel в commonMain sourceSet. В этом случае вам потребуется альтернативный фреймворк DI, например, Koin , kotlin-inject , Metro или Kodein . Все фреймворки DI, работающие с Kotlin Multiplatform, можно найти на сайте klibs.io .

Наблюдайте за потоками в SwiftUI

Наблюдение за потоками сопрограмм в SwiftUI напрямую не поддерживается. Однако вы можете использовать библиотеку KMP-NativeCoroutines или SKIE для реализации этой функции.