ViewModel SavedState¶
This artifact brings:
- Android Parcelable interface.
- The
@Parcelize
annotation from kotlin-parcelize compiler plugin. - SavedStateHandle class.
to Kotlin Multiplatform, so they can be used in common code. This is typically used for state/data preservation over Android configuration changes and system-initiated process death , when writing common code targeting Android.
1. Add dependency¶
- Add
mavenCentral()
torepositories
list inbuild.gradle.kts
/settings.gradle.kts
.
// settings.gradle.kts
dependencyResolutionManagement {
[...]
repositories {
mavenCentral()
[...]
}
}
- Add dependency to
build.gradle.kts
of your shared module (must useapi
configuration).
// build.gradle.kts
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api("io.github.hoc081098:kmp-viewmodel-savedstate:0.8.0")
}
}
}
}
- Expose
kmp-viewmodel-savedstate
toDarwin
native side.
// Cocoapods
kotlin {
cocoapods {
[...]
framework {
baseName = "shared"
export("io.github.hoc081098:kmp-viewmodel-savedstate:0.8.0") // required to expose the classes to iOS.
}
}
}
// -- OR --
// Kotlin/Native as an Apple framework
kotlin {
ios {
binaries {
framework {
baseName = "shared"
export("io.github.hoc081098:kmp-viewmodel-savedstate:0.8.0") // required to expose the classes to iOS.
}
}
}
}
- Optional: apply
kotlin-parcelize
if you want to use@Parcelize
annotation to generateParcelable
implementation for Android.
// build.gradle.kts
plugins {
id("kotlin-parcelize") // Apply the plugin for Android
}
// Since Kotlin 2.0.0, you must add the below code to your build.gradle.kts of the common/shared module (Kotlin Multiplatform module).
kotlin {
[...] // Other configurations
targets.configureEach {
val isAndroidTarget = platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.androidJvm
compilations.configureEach {
compileTaskProvider.configure {
compilerOptions {
if (isAndroidTarget) {
freeCompilerArgs.addAll(
"-P",
"plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.hoc081098.kmp.viewmodel.parcelable.Parcelize",
)
}
}
}
}
}
}
Snapshots of the development version are available in Sonatype's snapshots repository.
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
[...]
}
}
// build.gradle.kts
dependencies {
api("io.github.hoc081098:kmp-viewmodel-savedstate:0.7.2-SNAPSHOT")
}
2. Overview¶
public expect class SavedStateHandle {
public constructor(initialState: Map<String, Any?>)
public constructor()
public operator fun contains(key: String): Boolean
public operator fun <T> get(key: String): T?
public fun <T> getStateFlow(key: String, initialValue: T): StateFlow<T>
public fun keys(): Set<String>
public fun <T> remove(key: String): T?
public operator fun <T> set(key: String, value: T?): Unit
}
The SavedStateHandle
class provides some methods to get and set data.
On Android, it is a type alias of androidx.lifecycle.SavedStateHandle
.
On other platforms, it is simply a wrapper of the normal Map<String, Any?>
.
Because the limitation of Android platform, the data stored in SavedStateHandle
must be one of the
following types:
Type/Class support | Array support |
---|---|
double | double[] |
int | int[] |
long | long[] |
String | String[] |
byte | byte[] |
char | char[] |
CharSequence | CharSequence[] |
float | float[] |
Parcelable | Parcelable[] |
Serializable | Serializable[] |
short | short[] |
SparseArray | |
Binder | |
Bundle | |
ArrayList | |
Size (only in API 21+) | |
SizeF (only in API 21+) |
If the class does not extend one of those in the above list, consider making the class parcelable
by adding the @Parcelize
annotation.
See SavedStateHandle supported types docs
for more details.
3. Usage example¶
3.1. Kotlin common code¶
import com.hoc081098.kmp.viewmodel.SavedStateHandle
import com.hoc081098.kmp.viewmodel.ViewModel
import com.hoc081098.kmp.viewmodel.parcelable.Parcelable
import com.hoc081098.kmp.viewmodel.parcelable.Parcelize
@Parcelize
data class User(val id: Long, val name: String) : Parcelable
class UserViewModel(
private val savedStateHandle: SavedStateHandle,
private val getUserUseCase: suspend () -> User?,
) : ViewModel() {
val userStateFlow = savedStateHandle.getStateFlow<User?>(USER_KEY, null).wrap()
fun getUser() {
viewModelScope.launch {
try {
val user = getUserUseCase()
savedStateHandle[USER_KEY] = user
} catch (e: CancellationException) {
throw e
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
e.printStackTrace()
}
}
}
private companion object {
private const val USER_KEY = "user_key"
}
}
3.2. Darwin targets (Swift code)¶
import Foundation
import Combine
import shared
private actor FakeGetUserUseCaseActor {
private var count = 0
func call() async throws -> User? {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
self.count += 1
if self.count.isMultiple(of: 2) {
return nil
} else {
return User(id: Int64(count), name: "hoc081098")
}
}
}
private class FakeGetUserUseCase: KotlinSuspendFunction0 {
private let actor = FakeGetUserUseCaseActor()
func invoke() async throws -> Any? { try await self.`actor`.call() }
}
@MainActor
class IosUserViewModel: ObservableObject {
private let commonVm: UserViewModel = UserViewModel.init(
savedStateHandle: .init(),
getUserUseCase: FakeGetUserUseCase()
)
@Published private(set) var user: User?
init() {
self.commonVm.userStateFlow.subscribe(
scope: self.commonVm.viewModelScope,
onValue: { [weak self] in self?.user = $0 }
)
}
func getUser() { self.commonVm.getUser() }
deinit {
self.commonVm.clear()
}
}
For more details, please check kmp viewmodel sample.
4. Type-safe access¶
Please check Type-safe access to SavedStateHandle for more details.