راه‌اندازی 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 APIs برای دسترسی از 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()

سپس، در کد UI خود، می‌توانید 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 Lifecycle

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

در این مرحله، می‌توانید IosViewModelStoreOwner را به‌عنوان @StateObject در SwiftUI View نمونه‌سازی کنید و تابع 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 دردسترس نیست

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

ادغام با Hilt

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

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

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