KMP용 ViewModel 설정

AndroidX ViewModel은 공유 비즈니스 로직과 UI 구성요소 간에 명확한 계약을 설정하는 브리지 역할을 합니다. 이 패턴은 플랫폼 간 데이터 일관성을 보장하는 동시에 각 플랫폼의 고유한 모양에 맞게 UI를 맞춤설정할 수 있도록 지원합니다. Android에서는 Jetpack Compose를, iOS에서는 SwiftUI를 사용하여 UI를 계속 개발할 수 있습니다.

ViewModel 기본 문서에서 ViewModel 사용의 이점과 모든 기능을 자세히 알아보세요.

종속 항목 설정

프로젝트에서 KMP ViewModel을 설정하려면 libs.versions.toml 파일에서 종속 항목을 정의합니다.

[versions]
androidx-viewmodel = 2.9.3

[libraries]
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }

그런 다음 KMP 모듈의 build.gradle.kts 파일에 아티팩트를 추가하고 종속 항목을 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)
}

Swift에서 액세스할 수 있도록 ViewModel API 내보내기

기본적으로 코드베이스에 추가하는 라이브러리는 바이너리 프레임워크로 자동 내보내기되지 않습니다. API가 내보내지지 않으면 공유 코드(iosMain 또는 commonMain 소스 세트에서)에서 사용하는 경우에만 바이너리 프레임워크에서 사용할 수 있습니다. 이 경우 API에는 패키지 접두사가 포함됩니다. 예를 들어 ViewModel 클래스는 Lifecycle_viewmodelViewModel 클래스로 사용할 수 있습니다. 종속 항목 내보내기에 대한 자세한 내용은 종속 항목을 바이너리로 내보내기를 참고하세요.

환경을 개선하려면 iOS 바이너리 프레임워크를 정의하는 build.gradle.kts 파일에서 export 설정을 사용하여 ViewModel 종속 항목을 바이너리 프레임워크로 내보내세요. 그러면 Kotlin 코드에서와 마찬가지로 Swift 코드에서 직접 ViewModel API에 액세스할 수 있습니다.

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"
  }
}

(선택사항) JVM 데스크톱에서 viewModelScope 사용

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 문서를 참고하세요.

commonMain 또는 androidMain에서 ViewModel 사용

공유 commonMain에서 ViewModel 클래스를 사용하거나 androidMain sourceSet에서 사용하는 데 특별한 요구사항은 없습니다. 유일한 고려사항은 플랫폼별 API를 사용할 수 없고 이를 추상화해야 한다는 것입니다. 예를 들어 Android Application를 ViewModel 생성자 매개변수로 사용하는 경우 이 API를 추상화하여 마이그레이션해야 합니다.

플랫폼별 코드를 사용하는 방법에 관한 자세한 내용은 Kotlin 멀티플랫폼의 플랫폼별 코드를 참고하세요.

예를 들어 다음 스니펫에는 commonMain에 정의된 팩토리가 있는 ViewModel 클래스가 있습니다.

// 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
}

SwiftUI에서 ViewModel 사용

Android에서 ViewModel 수명 주기는 자동으로 처리되며 ComponentActivity, Fragment, NavBackStackEntry (Navigation 2) 또는 rememberViewModelStoreNavEntryDecorator (Navigation 3)로 범위가 지정됩니다. 하지만 iOS의 SwiftUI에는 AndroidX ViewModel에 상응하는 기본 제공 기능이 없습니다.

ViewModel을 SwiftUI 앱과 공유하려면 설정 코드를 추가해야 합니다.

제네릭에 도움이 되는 함수 만들기

일반 ViewModel 인스턴스를 인스턴스화하면 Android에서 클래스 참조 리플렉션 기능이 사용됩니다. Objective-C 제네릭은 Kotlin 또는 Swift의 모든 기능을 지원하지 않으므로 Swift에서 제네릭 유형의 ViewModel을 직접 가져올 수 없습니다.

이 문제를 해결하려면 일반 유형 대신 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 유형의 일반 함수를 작성하고 ObjCClassresolveViewModel 함수에 전달할 수 있는 T.self를 사용하면 됩니다.

ViewModel 범위를 SwiftUI 수명 주기에 연결

다음 단계는 ObservableObjectViewModelStoreOwner 인터페이스 (프로토콜)를 구현하는 IosViewModelStoreOwner를 만드는 것입니다. ObservableObject를 사용하는 이유는 이 클래스를 SwiftUI 코드에서 @StateObject로 사용할 수 있기 때문입니다.

// 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()
    }
}

이 소유자는 Android에서와 마찬가지로 여러 ViewModel 유형을 가져올 수 있습니다. 이러한 ViewModel의 수명 주기는 IosViewModelStoreOwner를 사용하는 화면이 초기화 해제되고 deinit을 호출할 때 삭제됩니다. 공식 문서에서 초기화 해제에 대해 자세히 알아보세요.

이 시점에서 SwiftUI 뷰에서 IosViewModelStoreOwner@StateObject로 인스턴스화하고 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 멀티플랫폼에서 사용할 수 없음

Android에서 사용할 수 있는 API 중 일부는 Kotlin Multiplatform에서 사용할 수 없습니다.

Hilt와 통합

Kotlin Multiplatform 프로젝트에서는 Hilt를 사용할 수 없으므로 commonMain sourceSet에서 @HiltViewModel 주석이 있는 ViewModel을 직접 사용할 수 없습니다. 이 경우 Koin, kotlin-inject, Metro, Kodein과 같은 대체 DI 프레임워크를 사용해야 합니다. Kotlin Multiplatform과 호환되는 모든 DI 프레임워크는 klibs.io에서 확인할 수 있습니다.

SwiftUI에서 흐름 관찰

SwiftUI에서 코루틴 흐름을 관찰하는 것은 직접 지원되지 않습니다. 하지만 KMP-NativeCoroutines 또는 SKIE 라이브러리를 사용하여 이 기능을 허용할 수 있습니다.