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

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

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

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

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

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

จากนั้นในโค้ด 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 ไม่รองรับฟีเจอร์ทั้งหมดของ Kotlin หรือ Swift คุณจึงไม่สามารถดึงข้อมูล ViewModel ของประเภทเจเนอริกจาก Swift ได้โดยตรง

เพื่อช่วยแก้ปัญหานี้ คุณสามารถสร้างฟังก์ชันตัวช่วยที่จะใช้ ObjCClass แทนประเภท Generics แล้วใช้ 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 คุณจึงไม่สามารถใช้ ViewModel กับคำอธิบายประกอบ @HiltViewModel ใน commonMain sourceSet ได้โดยตรง ในกรณีนี้ คุณต้องใช้เฟรมเวิร์ก DI อื่นๆ เช่น Koin, kotlin-inject, Metro หรือ Kodein คุณดูเฟรมเวิร์ก DI ทั้งหมดที่ทำงานร่วมกับ Kotlin Multiplatform ได้ที่ klibs.io

Observe Flows ใน SwiftUI

ระบบไม่รองรับการสังเกตโฟลว์ Coroutine ใน SwiftUI โดยตรง อย่างไรก็ตาม คุณสามารถใช้ไลบรารี KMP-NativeCoroutines หรือ SKIE เพื่ออนุญาตฟีเจอร์นี้ได้