Skip to content

Commit

Permalink
feat: compose saved state handle
Browse files Browse the repository at this point in the history
Added support to use composeValueManager inside an AAC ViewModel and restore values using SavedStateHandle
  • Loading branch information
programadorthi committed Jun 19, 2024
1 parent 7475731 commit 6028fcd
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dev.programadorthi.state.compose

import androidx.compose.runtime.SnapshotMutationPolicy
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.lifecycle.SavedStateHandle
import dev.programadorthi.state.core.ValueManager

internal class ComposeValueManagerSavedStateHandle<T>(
private val valueManager: ValueManager<T>,
private val policy: SnapshotMutationPolicy<T>,
private val savedStateHandle: SavedStateHandle,
stateRestorationKey: String,
saver: Saver<T, out Any>,
) : SaverScope, ValueManager<T> by valueManager {

private val valueManagerSaver = ValueManagerSaver(saver) { valueManager }
private val state = mutableStateOf(valueManager.value, policy)
private val key = "$KEY:$stateRestorationKey"
private val canBeSaved: Boolean

init {
val restoredValue = savedStateHandle.remove<Any>(key)

// By default SaveStateHandle throw exceptions when can't save the value in a Bundle
savedStateHandle[key] = valueManager.value
canBeSaved = true

valueManager.collect { newValue ->
state.value = newValue
savedStateHandle[key] = with(valueManagerSaver) {
save(valueManager)
}
}

if (restoredValue != null) {
valueManagerSaver.restore(restoredValue)
}
}

override var value: T
get() = state.value
set(value) {
val current = state.value
if (policy.equivalent(current, value).not()) {
valueManager.value = value
}
}

override fun canBeSaved(value: Any): Boolean = canBeSaved

private companion object {
private const val KEY = "dev.programadorthi.state.compose.ComposeValueManagerState.android"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dev.programadorthi.state.compose

import androidx.compose.runtime.SnapshotMutationPolicy
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.lifecycle.SavedStateHandle
import dev.programadorthi.state.core.ValueManager
import dev.programadorthi.state.core.extension.basicValueManager
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

public fun <T> composeValueManager(
initialValue: T,
stateRestorationKey: String,
savedStateHandle: SavedStateHandle,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
saver: Saver<T, out Any> = autoSaver(),
): ValueManager<T> = ComposeValueManagerSavedStateHandle(
valueManager = basicValueManager(initialValue),
policy = policy,
savedStateHandle = savedStateHandle,
stateRestorationKey = stateRestorationKey,
saver = saver,
)

public fun <T> composeValueManager(
initialValue: T,
savedStateHandle: SavedStateHandle,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
saver: Saver<T, out Any> = autoSaver(),
): ComposeValueManager<T> = basicValueManager(initialValue).asState(
savedStateHandle = savedStateHandle,
policy = policy,
saver = saver,
)

public fun <T> ValueManager<T>.asState(
savedStateHandle: SavedStateHandle,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
saver: Saver<T, out Any> = autoSaver(),
): ComposeValueManager<T> = PropertyDelegateProvider { _, property ->
val state = asState(
stateRestorationKey = property.name,
savedStateHandle = savedStateHandle,
policy = policy,
saver = saver,
)
object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value

override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
state.value = value
}
}
}

public fun <T> ValueManager<T>.asState(
stateRestorationKey: String,
savedStateHandle: SavedStateHandle,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
saver: Saver<T, out Any> = autoSaver(),
): ValueManager<T> = ComposeValueManagerSavedStateHandle(
valueManager = this,
policy = policy,
savedStateHandle = savedStateHandle,
stateRestorationKey = stateRestorationKey,
saver = saver,
)
40 changes: 40 additions & 0 deletions compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("com.android.library")
id("com.vanniktech.maven.publish")
}

Expand All @@ -9,6 +10,10 @@ applyBasicSetup()
darwinTargetsFramework()

kotlin {
androidTarget {
publishLibraryVariants("release")
}

sourceSets {
commonMain {
dependencies {
Expand All @@ -23,5 +28,40 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
}
}
androidMain {
dependencies {
implementation("androidx.activity:activity-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
}
}
}
}

android {
namespace = "dev.programadorthi.state.compose"
compileSdk = 34

defaultConfig {
minSdk = 23
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}

sourceSets["main"].manifest.srcFile("src/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/res")
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal class ComposeValueManagerState<T>(

private val valueManagerSaver = ValueManagerSaver(saver) { valueManager }
private val state = mutableStateOf(valueManager.value, policy)
private val key = "$KEY:$stateRestorationKey"

private var entry: SaveableStateRegistry.Entry? = null

Expand Down Expand Up @@ -55,17 +56,17 @@ internal class ComposeValueManagerState<T>(
}

private fun tryRestoreAndRegister() {
val keyToRestore = stateRestorationKey.takeIf { it.isNullOrBlank().not() } ?: return
if (stateRestorationKey.isNullOrBlank()) return
val registry = stateRegistry ?: return
registry.consumeRestored(keyToRestore)?.let { consumed ->
registry.consumeRestored(key)?.let { consumed ->
valueManagerSaver.restore(consumed)
}

register()
}

private fun register() {
val keyToRestore = stateRestorationKey.takeIf { it.isNullOrBlank().not() } ?: return
if (stateRestorationKey.isNullOrBlank()) return
val registry = stateRegistry ?: return

val saveable = {
Expand All @@ -77,7 +78,11 @@ internal class ComposeValueManagerState<T>(
"$value cannot be saved using the current SaveableStateRegistry"
}
if (registry.canBeSaved(toSave)) {
entry = registry.registerProvider(keyToRestore, saveable)
entry = registry.registerProvider(key, saveable)
}
}

private companion object {
private const val KEY = "dev.programadorthi.state.compose.ComposeValueManagerState"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

private const val KEY = "dev.programadorthi.state.core.AndroidValueManagerState"

private fun String.compoundKey(): String = "$KEY:$this"

public typealias AndroidValueManager<T> = PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>>

public fun <T> androidValueManager(
Expand All @@ -32,7 +36,7 @@ public fun <T> androidValueManager(
)
val state = AndroidValueManagerState(
valueManager = valueManager,
stateRestorationKey = stateRestorationKey,
stateRestorationKey = stateRestorationKey.compoundKey(),
savedStateHandle = savedStateHandle,
saver = saver,
)
Expand Down Expand Up @@ -162,15 +166,16 @@ private fun <T> ViewModelStoreOwner.ownerValueManager(
changeHandler = changeHandler,
errorHandler = errorHandler,
)
val key = stateRestorationKey.compoundKey()
val provider = ViewModelProvider(
owner = this,
factory = AndroidValueManagerStateFactory(
stateRestorationKey = stateRestorationKey,
stateRestorationKey = key,
valueManager = valueManager,
saver = saver,
)
)
val state = provider[stateRestorationKey, AndroidValueManagerState::class.java]
val state = provider[key, AndroidValueManagerState::class.java]
@Suppress("UNCHECKED_CAST")
state as AndroidValueManagerState<T>
state.valueManager
Expand Down
1 change: 1 addition & 0 deletions samples/compose/norris-facts/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ dependencies {
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="false"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity android:name=".MainActivity" android:exported="true">
android:allowBackup="false"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ComposeActivity" android:exported="false" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.programadorthi.android

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

class ComposeActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Content()
}
}

@Composable
private fun Content(
viewModel: ComposeViewModel = viewModel()
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("Counter: ${viewModel.state}")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = viewModel::decrement) {
Text("Decrement")
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = viewModel::increment) {
Text("Increment")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.programadorthi.android

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import dev.programadorthi.state.compose.composeValueManager

class ComposeViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {

private var composeState by composeValueManager(0, savedStateHandle)

val state: Int
get() = composeState

fun increment() {
composeState++
}

fun decrement() {
composeState--
}
}
Loading

0 comments on commit 6028fcd

Please sign in to comment.