Skip to content

ViewModel SavedState

maven-central

This artifact brings:

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() to repositories list in build.gradle.kts/settings.gradle.kts.
// settings.gradle.kts
dependencyResolutionManagement {
  [...]
  repositories {
    mavenCentral()
    [...]
  }
}
  • Add dependency to build.gradle.kts of your shared module (must use api configuration).
// build.gradle.kts
kotlin {
  sourceSets {
    val commonMain by getting {
      dependencies {
        api("io.github.hoc081098:kmp-viewmodel-savedstate:0.7.1")
      }
    }
  }
}
  • Expose kmp-viewmodel-savedstate to Darwin native side.
// Cocoapods
kotlin {
  cocoapods {
    [...]
    framework {
      baseName = "shared"
      export("io.github.hoc081098:kmp-viewmodel-savedstate:0.7.1") // 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.7.1") // required to expose the classes to iOS.
      }
    }
  }
}
  • Optional: apply kotlin-parcelize if you want to use @Parcelize annotation to generate Parcelable implementation for Android.
// build.gradle.kts
plugins {
  id("kotlin-parcelize") // Apply the plugin for Android
}
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.