ViewModel را برای KMP تنظیم کنید

AndroidX ViewModel به عنوان یک پل عمل می‌کند و یک قرارداد واضح بین منطق کسب‌وکار مشترک شما و اجزای رابط کاربری شما برقرار می‌کند. این الگو به تضمین سازگاری داده‌ها در پلتفرم‌های مختلف کمک می‌کند، در حالی که امکان سفارشی‌سازی رابط‌های کاربری را برای ظاهر متمایز هر پلتفرم فراهم می‌کند. می‌توانید توسعه رابط کاربری خود را با Jetpack Compose در اندروید و 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 را تعریف می‌کنید، به چارچوب دودویی export کنید، که باعث می‌شود 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 دسکتاپ

هنگام اجرای کوروتین‌ها در یک 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 مخصوص پلتفرمی استفاده کنید و باید آنها را انتزاعی (abstract) کنید. به عنوان مثال، اگر از یک Application اندروید به عنوان پارامتر سازنده ViewModel استفاده می‌کنید، باید با انتزاعی کردن آن، از این API مهاجرت کنید.

اطلاعات بیشتر در مورد نحوه استفاده از کد مخصوص پلتفرم در platform-specific code in Kotlin Multiplatform موجود است.

برای مثال، در قطعه کد زیر یک کلاس ViewModel به همراه factory آن وجود دارد که در 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

در اندروید، چرخه حیات ViewModel به طور خودکار مدیریت و در محدوده ComponentActivity ، Fragment ، NavBackStackEntry (Navigation 2) یا rememberViewModelStoreNavEntryDecorator (Navigation 3) قرار می‌گیرد. با این حال، SwiftUI در iOS هیچ معادل داخلی برای AndroidX ViewModel ندارد.

برای اشتراک‌گذاری ViewModel با برنامه SwiftUI خود، باید کد راه‌اندازی را اضافه کنید.

یک تابع برای کمک به جنریک‌ها ایجاد کنید

نمونه‌سازی یک نمونه ViewModel ژنریک از ویژگی انعکاس ارجاع کلاس در اندروید استفاده می‌کند. از آنجا که ژنریک‌های Objective-C از تمام ویژگی‌های Kotlin یا Swift پشتیبانی نمی‌کنند ، نمی‌توانید مستقیماً یک ViewModel از نوع ژنریک را از Swift بازیابی کنید.

برای کمک به حل این مشکل، می‌توانید یک تابع کمکی ایجاد کنید که به جای نوع generics از 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()
    }
}

این مالک (owner) امکان بازیابی چندین نوع ViewModel را فراهم می‌کند، مشابه اندروید. چرخه حیات آن ViewModelها زمانی پاک می‌شود که صفحه نمایش با استفاده از IosViewModelStoreOwner مقداردهی اولیه (deinitialized) شود و deinit را فراخوانی کند. می‌توانید اطلاعات بیشتر در مورد deinitialization را در مستندات رسمی بیابید.

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

در کاتلین چند پلتفرمی موجود نیست

برخی از APIهایی که در اندروید موجود هستند، در کاتلین چند پلتفرمی در دسترس نیستند.

ادغام با هیلت

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

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

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