راه‌اندازی ViewModel برای KMP

‫AndroidX ViewModel به‌عنوان پلی ارتباطی عمل می‌کند و قراردادی واضح بین منطق کسب‌وکار مشترک شما و عناصر رابط کاربری‌تان ایجاد می‌کند. این الگو به تضمین ثبات داده‌ها در سراسر پلاتفرم‌ها کمک می‌کند و درعین‌حال به واسط‌های کاربر امکان می‌دهد برای ظاهر متمایز هر پلاتفرم سفارشی‌سازی شوند. می‌توانید توسعه میانای کاربر خود را با Jetpack Compose در Android و SwiftUI در iOS ادامه دهید.

درباره مزایای استفاده از ViewModel و همه ویژگی‌های مستندات اصلی ViewModel بیشتر بخوانید.

راه‌اندازی وابستگی‌ها

برای راه‌اندازی KMP ViewModel در پروژه خود، وابستگی را در فایل libs.versions.toml تعریف کنید:

[versions]
androidx-viewmodel = 2.11.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)
}

صادر کردن «میاناهای برنامه‌سازی کاربردی ViewModel» برای دسترسی از Swift

به‌طور پیش‌فرض، هر کتابخانه‌ای که به پایگاه کد خود اضافه می‌کنید به‌طور خودکار به چارچوب باینری صادر نمی‌شود. اگر «میاناهای برنامه‌سازی کاربردی» صادر نشوند، فقط درصورتی از چارچوب باینری دردسترس هستند که از آن‌ها در کد مشترک (از مجموعه منبع iosMain یا commonMain) استفاده کنید. در این حالت، «میاناهای برنامه‌سازی کاربردی» حاوی پیشوند بسته خواهد بود، برای مثال کلاس ViewModel به‌عنوان کلاس Lifecycle_viewmodelViewModel دردسترس خواهد بود. برای اطلاعات بیشتر درباره صادر کردن وابستگی‌ها، صادر کردن وابستگی‌ها به باینری‌ها را بررسی کنید.

برای بهبود تجربه، می‌توانید وابستگی ViewModel را بااستفاده از تنظیم export در فایل build.gradle.kts که در آن چارچوب باینری iOS را تعریف می‌کنید به چارچوب باینری صادر کنید، که باعث می‌شود «میاناهای برنامه‌سازی کاربردی» 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 منبع‌مجموعه وجود ندارد. تنها نکته‌ای که باید درنظر بگیرید این است که نمی‌توانید از هیچ‌یک از میاناهای برنامه‌سازی کاربردی مختص پلاتفرم استفاده کنید و باید آن‌ها را انتزاعی کنید. برای مثال، اگر از Android Application به‌عنوان پارامتر سازنده ViewModel استفاده می‌کنید، باید با انتزاع کردن این API از آن مهاجرت کنید.

اطلاعات بیشتر درباره نحوه استفاده از کد مختص پلاتفرم در کد مختص پلاتفرم در Kotlin چندپلاتفرمی دردسترس است.

برای مثال، در گزیده زیر کلاس 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 معادل داخلی برای 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. وقتی صفحه‌نمایش استفاده‌کننده از IosViewModelStoreOwner غیرمقداردهی اولیه می‌شود و deinit را فرا می‌خواند، چرخه حیات آن ViewModels پاک می‌شود. در اسناد رسمی می‌توانید درباره مقداردهی اولیه بیشتر بدانید.

در این مرحله، می‌توانید 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 چندپلاتفرمی دردسترس نیست

برخی‌از APIهایی که در Android دردسترس هستند در Kotlin Multiplatform دردسترس نیستند.

یکپارچه‌سازی با Hilt

چون Hilt برای پروژه‌های Kotlin چندپلاتفرمی دردسترس نیست، نمی‌توانید مستقیماً از ViewModels با حاشیه @HiltViewModel در commonMain sourceSet استفاده کنید. در این صورت باید از چارچوب DI جایگزین دیگری استفاده کنید، برای مثال، Koin، kotlin-inject، Metro، یا Kodein. می‌توانید همه چارچوب‌های DI را که با Kotlin Multiplatform کار می‌کنند در klibs.io پیدا کنید.

مشاهده جریان‌ها در SwiftUI

مشاهده «جریان‌های» روتین‌های همکار در SwiftUI مستقیماً پشتیبانی نمی‌شود. بااین‌حال، می‌توانید از کتابخانه KMP-NativeCoroutines یا SKIE برای فعال کردن این ویژگی استفاده کنید.