From 2d9775ea473f1dfd14fdda5988227bd73726df64 Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Thu, 5 Jan 2023 19:22:41 +0900 Subject: [PATCH 1/4] Remove unused dependency for test --- savedstate-ktx/build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/savedstate-ktx/build.gradle.kts b/savedstate-ktx/build.gradle.kts index 3a0dfe5..7abe637 100644 --- a/savedstate-ktx/build.gradle.kts +++ b/savedstate-ktx/build.gradle.kts @@ -9,8 +9,8 @@ plugins { android { namespace = "${project.group}" - compileSdk = 33 + defaultConfig { minSdk = 21 targetSdk = 33 @@ -44,7 +44,6 @@ dependencies { androidTestImplementation("androidx.test.ext:junit-ktx:1.1.4") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") androidTestImplementation("com.google.truth:truth:1.1.3") - androidTestImplementation("androidx.appcompat:appcompat:1.5.1") androidTestImplementation("androidx.activity:activity-ktx:1.6.1") } From c47e2be2664af6c5f442dfa5b465b4c1b2640b91 Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Thu, 5 Jan 2023 19:23:12 +0900 Subject: [PATCH 2/4] Create experimental api marker --- .../woong/savedstate/ExperimentalSavedStateKtxApi.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 savedstate-ktx/src/main/java/io/woong/savedstate/ExperimentalSavedStateKtxApi.kt diff --git a/savedstate-ktx/src/main/java/io/woong/savedstate/ExperimentalSavedStateKtxApi.kt b/savedstate-ktx/src/main/java/io/woong/savedstate/ExperimentalSavedStateKtxApi.kt new file mode 100644 index 0000000..15cec71 --- /dev/null +++ b/savedstate-ktx/src/main/java/io/woong/savedstate/ExperimentalSavedStateKtxApi.kt @@ -0,0 +1,11 @@ +package io.woong.savedstate + +/** + * Marker for experimental features of `savedstate-ktx` library. + */ +@RequiresOptIn( + message = "This is experimental API. It could be removed or changed in the future.", + level = RequiresOptIn.Level.ERROR +) +@Retention(AnnotationRetention.BINARY) +public annotation class ExperimentalSavedStateKtxApi From fe11c9567286f925b1cfbdb8bbeddecd7f484ef5 Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Thu, 5 Jan 2023 20:59:07 +0900 Subject: [PATCH 3/4] Create experimental mutableStateFlow delegate --- .../savedstate/DelegatedStateFlowTest.kt | 44 ++++++++++- .../io/woong/savedstate/DelegatedStateFlow.kt | 75 +++++++++++++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/savedstate-ktx/src/androidTest/java/io/woong/savedstate/DelegatedStateFlowTest.kt b/savedstate-ktx/src/androidTest/java/io/woong/savedstate/DelegatedStateFlowTest.kt index 6588eb4..634c593 100644 --- a/savedstate-ktx/src/androidTest/java/io/woong/savedstate/DelegatedStateFlowTest.kt +++ b/savedstate-ktx/src/androidTest/java/io/woong/savedstate/DelegatedStateFlowTest.kt @@ -4,9 +4,11 @@ import androidx.activity.ComponentActivity import androidx.activity.viewModels import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import androidx.test.core.app.launchActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.junit.Test import org.junit.runner.RunWith @@ -14,7 +16,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) public class DelegatedStateFlowTest { @Test - public fun savedStateDelegate() { + public fun stateFlowDelegate() { val scenario = launchActivity() scenario.onActivity { activity -> val viewModel = activity.viewModel @@ -24,12 +26,42 @@ public class DelegatedStateFlowTest { viewModel.savedStateHandle.get("stateFlow") ).isEqualTo("init") - viewModel.savedStateHandle["stateFlow"] = "new" + viewModel.savedStateHandle["stateFlow"] = "first" + assertThat(viewModel.stateFlow.value).isEqualTo("first") + assertThat( + viewModel.savedStateHandle.get("stateFlow") + ).isEqualTo("first") - assertThat(viewModel.stateFlow.value).isEqualTo("new") + viewModel.savedStateHandle["stateFlow"] = "second" + assertThat(viewModel.stateFlow.value).isEqualTo("second") assertThat( viewModel.savedStateHandle.get("stateFlow") - ).isEqualTo("new") + ).isEqualTo("second") + } + } + + @Test + public fun mutableStateFlowDelegate() { + val scenario = launchActivity() + scenario.onActivity { activity -> + val viewModel = activity.viewModel + + assertThat(viewModel.mutableStateFlow.value).isEqualTo("init") + assertThat( + viewModel.savedStateHandle.get("mutableStateFlow") + ).isEqualTo("init") + + viewModel.mutableStateFlow.value = "first" + assertThat(viewModel.mutableStateFlow.value).isEqualTo("first") + assertThat( + viewModel.savedStateHandle.get("mutableStateFlow") + ).isEqualTo("first") + + viewModel.mutableStateFlow.value = "second" + assertThat(viewModel.mutableStateFlow.value).isEqualTo("second") + assertThat( + viewModel.savedStateHandle.get("mutableStateFlow") + ).isEqualTo("second") } } @@ -39,5 +71,9 @@ public class DelegatedStateFlowTest { public class TestViewModel(public val savedStateHandle: SavedStateHandle) : ViewModel() { public val stateFlow: StateFlow by savedStateHandle.stateFlow("init") + + @OptIn(ExperimentalSavedStateKtxApi::class) + public val mutableStateFlow: MutableStateFlow + by savedStateHandle.mutableStateFlow("init", viewModelScope) } } diff --git a/savedstate-ktx/src/main/java/io/woong/savedstate/DelegatedStateFlow.kt b/savedstate-ktx/src/main/java/io/woong/savedstate/DelegatedStateFlow.kt index 6ccd0e2..e64671f 100644 --- a/savedstate-ktx/src/main/java/io/woong/savedstate/DelegatedStateFlow.kt +++ b/savedstate-ktx/src/main/java/io/woong/savedstate/DelegatedStateFlow.kt @@ -1,7 +1,12 @@ package io.woong.savedstate +import androidx.lifecycle.Observer import androidx.lifecycle.SavedStateHandle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.launch import kotlin.reflect.KProperty /** @@ -13,6 +18,20 @@ public fun SavedStateHandle.stateFlow(initialValue: T): StateFlowDelegatePro return StateFlowDelegateProvider(savedStateHandle = this, initialValue) } +/** + * Returns a delegated [MutableStateFlow] that hande value stored in the [SavedStateHandle]. + * + * @param initialValue Initial value of this [MutableStateFlow]. + * @param coroutineScope A [CoroutineScope] to control [MutableStateFlow]. + */ +@ExperimentalSavedStateKtxApi +public fun SavedStateHandle.mutableStateFlow( + initialValue: T, + coroutineScope: CoroutineScope +): MutableStateFlowDelegateProvider { + return MutableStateFlowDelegateProvider(savedStateHandle = this, initialValue, coroutineScope) +} + /** * Internal delegated [StateFlow] provider. */ @@ -43,3 +62,59 @@ public class DelegatedStateFlow( return stateFlow } } + +/** + * Internal delegated [MutableStateFlow] provider. + */ +@ExperimentalSavedStateKtxApi +public class MutableStateFlowDelegateProvider( + private val savedStateHandle: SavedStateHandle, + private val initialValue: T, + private val coroutineScope: CoroutineScope +) { + public operator fun provideDelegate( + self: Any?, + property: KProperty<*> + ): DelegatedMutableStateFlow { + val key = property.name + return DelegatedMutableStateFlow(savedStateHandle, key, initialValue, coroutineScope) + } +} + +/** + * Internal implementation of delegated [MutableStateFlow]. + */ +@ExperimentalSavedStateKtxApi +public class DelegatedMutableStateFlow( + savedStateHandle: SavedStateHandle, + key: String, + initialValue: T, + coroutineScope: CoroutineScope +) { + private val mutableStateFlow: MutableStateFlow + + init { + val liveData = savedStateHandle.getLiveData(key, initialValue) + val stateFlow = MutableStateFlow(initialValue) + val observer = Observer { value -> + if (value != stateFlow.value) { + stateFlow.value = value + } + } + liveData.observeForever(observer) + coroutineScope.launch { + stateFlow.onCompletion { + liveData.removeObserver(observer) + }.collect { value -> + if (value != liveData.value) { + liveData.value = value + } + } + } + mutableStateFlow = stateFlow + } + + public operator fun getValue(self: Any?, property: KProperty<*>): MutableStateFlow { + return mutableStateFlow + } +} From d203a0cf6c80c398b2ccb046bd9cdf8e1c61c465 Mon Sep 17 00:00:00 2001 From: Jaewoong Cheon Date: Sat, 7 Jan 2023 13:06:20 +0900 Subject: [PATCH 4/4] Update readme --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 511776e..24ba47d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # SavedState KTX -![maven-central](https://img.shields.io/maven-central/v/io.woong.savedstate/savedstate-ktx) +![android-sdk](https://img.shields.io/badge/android-21+-brightgreen?logo=android) ![kotlin-version](https://img.shields.io/badge/kotlin-1.7.20-blueviolet?logo=kotlin) +![maven-central](https://img.shields.io/maven-central/v/io.woong.savedstate/savedstate-ktx) ![license](https://img.shields.io/badge/license-MIT-blue) [![test](https://github.com/cheonjaewoong/savedstate-ktx/actions/workflows/test.yaml/badge.svg)](https://github.com/cheonjaewoong/savedstate-ktx/actions/workflows/test.yaml) @@ -70,6 +71,16 @@ class SampleViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { Like delegated livedata, you can use delegated stateflow like this. +```kotlin +class SampleViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { + @OptIn(ExperimentalSavedStateKtxApi::class) + val stateFlow: MutableStateFlow by savedStateHandle.mutableStateFlow("init") +} +``` + +Delegated mutable stateflow is currently experimental feature. +You can use like above sample. + ## License -SavedState KTX is published under the [MIT License](./LICENSE.txt). +SavedState KTX is under the [MIT License](./LICENSE.txt).