Skip to content
This repository has been archived by the owner on Mar 2, 2024. It is now read-only.

Commit

Permalink
Merge pull request #12 from cheonjaewoong/mutablestateflow
Browse files Browse the repository at this point in the history
Add new feature, `SavedStateHandle.mutableStateFlow`
  • Loading branch information
cheonjaeung authored Jan 7, 2023
2 parents dd87176 + d203a0c commit 6d078c9
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 7 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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<String> 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).
1 change: 0 additions & 1 deletion savedstate-ktx/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ dependencies {
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("com.google.truth:truth:1.1.3")
androidTestImplementation("androidx.appcompat:appcompat:1.5.1")
androidTestImplementation("androidx.activity:activity-ktx:1.6.1")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ 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

@RunWith(AndroidJUnit4::class)
public class DelegatedStateFlowTest {
@Test
public fun savedStateDelegate() {
public fun stateFlowDelegate() {
val scenario = launchActivity<TestActivity>()
scenario.onActivity { activity ->
val viewModel = activity.viewModel
Expand All @@ -24,12 +26,42 @@ public class DelegatedStateFlowTest {
viewModel.savedStateHandle.get<String>("stateFlow")
).isEqualTo("init")

viewModel.savedStateHandle["stateFlow"] = "new"
viewModel.savedStateHandle["stateFlow"] = "first"
assertThat(viewModel.stateFlow.value).isEqualTo("first")
assertThat(
viewModel.savedStateHandle.get<String>("stateFlow")
).isEqualTo("first")

assertThat(viewModel.stateFlow.value).isEqualTo("new")
viewModel.savedStateHandle["stateFlow"] = "second"
assertThat(viewModel.stateFlow.value).isEqualTo("second")
assertThat(
viewModel.savedStateHandle.get<String>("stateFlow")
).isEqualTo("new")
).isEqualTo("second")
}
}

@Test
public fun mutableStateFlowDelegate() {
val scenario = launchActivity<TestActivity>()
scenario.onActivity { activity ->
val viewModel = activity.viewModel

assertThat(viewModel.mutableStateFlow.value).isEqualTo("init")
assertThat(
viewModel.savedStateHandle.get<String>("mutableStateFlow")
).isEqualTo("init")

viewModel.mutableStateFlow.value = "first"
assertThat(viewModel.mutableStateFlow.value).isEqualTo("first")
assertThat(
viewModel.savedStateHandle.get<String>("mutableStateFlow")
).isEqualTo("first")

viewModel.mutableStateFlow.value = "second"
assertThat(viewModel.mutableStateFlow.value).isEqualTo("second")
assertThat(
viewModel.savedStateHandle.get<String>("mutableStateFlow")
).isEqualTo("second")
}
}

Expand All @@ -39,5 +71,9 @@ public class DelegatedStateFlowTest {

public class TestViewModel(public val savedStateHandle: SavedStateHandle) : ViewModel() {
public val stateFlow: StateFlow<String> by savedStateHandle.stateFlow("init")

@OptIn(ExperimentalSavedStateKtxApi::class)
public val mutableStateFlow: MutableStateFlow<String>
by savedStateHandle.mutableStateFlow("init", viewModelScope)
}
}
Original file line number Diff line number Diff line change
@@ -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

/**
Expand All @@ -13,6 +18,20 @@ public fun <T> 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 <T> SavedStateHandle.mutableStateFlow(
initialValue: T,
coroutineScope: CoroutineScope
): MutableStateFlowDelegateProvider<T> {
return MutableStateFlowDelegateProvider(savedStateHandle = this, initialValue, coroutineScope)
}

/**
* Internal delegated [StateFlow] provider.
*/
Expand Down Expand Up @@ -43,3 +62,59 @@ public class DelegatedStateFlow<T>(
return stateFlow
}
}

/**
* Internal delegated [MutableStateFlow] provider.
*/
@ExperimentalSavedStateKtxApi
public class MutableStateFlowDelegateProvider<T>(
private val savedStateHandle: SavedStateHandle,
private val initialValue: T,
private val coroutineScope: CoroutineScope
) {
public operator fun provideDelegate(
self: Any?,
property: KProperty<*>
): DelegatedMutableStateFlow<T> {
val key = property.name
return DelegatedMutableStateFlow(savedStateHandle, key, initialValue, coroutineScope)
}
}

/**
* Internal implementation of delegated [MutableStateFlow].
*/
@ExperimentalSavedStateKtxApi
public class DelegatedMutableStateFlow<T>(
savedStateHandle: SavedStateHandle,
key: String,
initialValue: T,
coroutineScope: CoroutineScope
) {
private val mutableStateFlow: MutableStateFlow<T>

init {
val liveData = savedStateHandle.getLiveData(key, initialValue)
val stateFlow = MutableStateFlow(initialValue)
val observer = Observer<T?> { 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<T> {
return mutableStateFlow
}
}
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 6d078c9

Please sign in to comment.