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

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

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

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

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

[versions]
androidx-viewmodel = 2.10.0

[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 будут содержать префикс пакета, например, класс 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 sourceSet. Единственное ограничение заключается в том, что нельзя использовать какие-либо 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 (Navigation 2) или rememberViewModelStoreNavEntryDecorator (Navigation 3). Однако SwiftUI на iOS не имеет встроенного эквивалента для ViewModel AndroidX.

Чтобы использовать 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, вы не можете напрямую использовать ViewModels с аннотацией @HiltViewModel в commonMain sourceSet. В этом случае вам потребуется использовать альтернативный фреймворк внедрения зависимостей, например, Koin , kotlin-inject , Metro или Kodein . Все фреймворки внедрения зависимостей, работающие с Kotlin Multiplatform, можно найти на klibs.io .

Отслеживание потоков в SwiftUI

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