ตั้งค่า ViewModel สำหรับ KMP

ViewModel ของ AndroidX ทำหน้าที่เป็นตัวเชื่อม โดยสร้างสัญญาที่ชัดเจนระหว่าง ตรรกะทางธุรกิจที่แชร์กับคอมโพเนนต์ UI รูปแบบนี้ช่วยให้มั่นใจได้ว่าข้อมูลจะสอดคล้องกันในทุกแพลตฟอร์ม ขณะเดียวกันก็ช่วยให้ปรับแต่ง UI ให้มีลักษณะเฉพาะของแต่ละแพลตฟอร์มได้ คุณสามารถพัฒนา UI ต่อไปได้ด้วย Jetpack Compose ใน Android และ SwiftUI ใน iOS

อ่านเพิ่มเติมเกี่ยวกับประโยชน์ของการใช้ ViewModel และฟีเจอร์ทั้งหมดในเอกสารประกอบหลัก สำหรับ ViewModel

ตั้งค่าทรัพยากร Dependency

หากต้องการตั้งค่า KMP ViewModel ในโปรเจ็กต์ ให้กำหนดทรัพยากร Dependency ในไฟล์ 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 และประกาศทรัพยากร Dependency เป็น api เนื่องจากระบบจะส่งออกทรัพยากร Dependency นี้ไปยัง เฟรมเวิร์กไบนารี

// 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 API เพื่อให้เข้าถึงได้จาก Swift

โดยค่าเริ่มต้น ระบบจะไม่ส่งออกไลบรารีที่คุณเพิ่มลงในโค้ดเบสไปยังเฟรมเวิร์กไบนารีโดยอัตโนมัติ หากไม่ได้ส่งออก API จะใช้ได้จากเฟรมเวิร์กไบนารีเท่านั้นในกรณีที่คุณใช้ API ในโค้ดที่ใช้ร่วมกัน (จากชุดแหล่งที่มา iosMain หรือ commonMain) ในกรณีดังกล่าว API จะมีคำนำหน้าของแพ็กเกจ เช่น คลาส ViewModel จะพร้อมใช้งานเป็นคลาส Lifecycle_viewmodelViewModel ดูข้อมูลเพิ่มเติมเกี่ยวกับการส่งออกการอ้างอิงได้ที่การส่งออกการอ้างอิงไปยัง ไบนารี

หากต้องการปรับปรุงประสบการณ์การใช้งาน คุณสามารถส่งออกการอ้างอิง ViewModel ไปยังเฟรมเวิร์กไบนารีได้ โดยใช้exportการตั้งค่าในไฟล์ build.gradle.kts ซึ่งคุณจะกำหนดเฟรมเวิร์กไบนารีของ iOS ได้ ซึ่งจะทำให้เข้าถึง ViewModel API ได้ โดยตรงจากโค้ด 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 ใน shared commonMain หรือจาก sourceSet ของ androidMain ข้อควรพิจารณามีเพียงข้อเดียวคือ คุณไม่สามารถใช้ API เฉพาะแพลตฟอร์มใดๆ และต้องแยก API เหล่านั้นออก ตัวอย่างเช่น หากคุณใช้ Android Application เป็นพารามิเตอร์ของตัวสร้าง ViewModel คุณจะต้องย้ายข้อมูลออกจาก API นี้โดยการแยกข้อมูล

ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีใช้โค้ดเฉพาะแพลตฟอร์มได้ที่ โค้ดเฉพาะแพลตฟอร์มใน 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()

จากนั้นในโค้ด 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 ไม่มี ViewModel ของ AndroidX ที่เทียบเท่าในตัว

หากต้องการแชร์ ViewModel กับแอป SwiftUI คุณต้องเพิ่มโค้ดการตั้งค่าบางอย่าง

สร้างฟังก์ชันเพื่อช่วยจัดการกับ Generics

การสร้างอินสแตนซ์ ViewModel ทั่วไปจะใช้ฟีเจอร์การสะท้อนการอ้างอิงคลาสใน Android เนื่องจาก Objective-C generics ไม่รองรับฟีเจอร์ทั้งหมดของ 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 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

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 ไม่รองรับการสังเกตการณ์ Coroutine Flow โดยตรง อย่างไรก็ตาม คุณสามารถใช้ไลบรารี KMP-NativeCoroutines หรือ SKIE เพื่ออนุญาตฟีเจอร์นี้ได้