commit d5ae708c037d3cc3d98e439806c9fefc4266954f
parent dd1af534d51e5a4baf0c40ffddd2356b989e6be8
Author: pollymce <pmceldowney@mozilla.com>
Date: Thu, 8 Jan 2026 09:52:14 +0000
Bug 1988984 - introduce StateFlow to Store. r=android-reviewers,sfamisa
Differential Revision: https://phabricator.services.mozilla.com/D278109
Diffstat:
2 files changed, 84 insertions(+), 4 deletions(-)
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt
@@ -6,6 +6,9 @@ package mozilla.components.lib.state
import androidx.annotation.CheckResult
import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import java.lang.ref.WeakReference
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
@@ -30,13 +33,20 @@ open class Store<S : State, A : Action>(
@VisibleForTesting
internal val subscriptions = Collections.newSetFromMap(ConcurrentHashMap<Subscription<S, A>, Boolean>())
- @Volatile private var currentState = initialState
+ private val mutableStateFlow = MutableStateFlow(initialState)
/**
* The current [State].
*/
val state: S
- get() = currentState
+ get() = mutableStateFlow.value
+
+ /**
+ * An observable flow which will emit the store state as it updates.
+ *
+ * @return the current state as a [StateFlow]
+ */
+ val stateFlow: StateFlow<S> = mutableStateFlow.asStateFlow()
/**
* Registers an [Observer] function that will be invoked whenever the [State] changes.
@@ -74,8 +84,8 @@ open class Store<S : State, A : Action>(
if (reducerChain == null) {
var chain: (A) -> Unit = { action ->
val newState = reducer(state, action)
- if (newState != currentState) {
- currentState = newState
+ if (newState != mutableStateFlow.value) {
+ mutableStateFlow.value = newState
subscriptions.forEach { subscription -> subscription.dispatch(newState) }
}
}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreStateFlowTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreStateFlowTest.kt
@@ -0,0 +1,70 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package mozilla.components.lib.state
+
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class StoreStateFlowTest {
+
+ @Test
+ fun `initial state is set in store`() {
+ val store = Store(
+ TestState(counter = 4),
+ ::reducer,
+ )
+ assertEquals(4, store.state.counter)
+ }
+
+ @Test
+ fun `can increment state in store`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+ assertEquals(0, store.state.counter)
+ store.dispatch(TestAction.IncrementAction)
+ assertEquals(1, store.state.counter)
+ }
+
+ @Test
+ fun `stateflow exposes incrementing state`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+ assertEquals(0, store.stateFlow.value.counter)
+ store.dispatch(TestAction.IncrementAction)
+ store.dispatch(TestAction.IncrementAction)
+ assertEquals(2, store.stateFlow.value.counter)
+ }
+
+ @Test
+ fun `can collect from stateflow`() = runTest {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+ var counter = 0
+ assertEquals(0, counter)
+ backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
+ store.stateFlow.collect {
+ counter = it.counter
+ }
+ }
+ store.dispatch(TestAction.IncrementAction)
+ store.dispatch(TestAction.IncrementAction)
+ store.dispatch(TestAction.IncrementAction)
+ testScheduler.advanceUntilIdle()
+ assertEquals(3, counter)
+ }
+}