SysUiViewModel.hydratedStateOf
Adds hydratedStateOf - a utility to help developers define a "hydrated"
(kept up-to-date) snapshot state (AKA compose State) property with a
single line of code and without needing to add a collector in the
activation code.
Bug: 354269846
Test: unit tests added
Test: manually verified with the next CL
Flag: NONE unused utility (in this CL)
Change-Id: Ibe0ccc623253d6df76cdef5c0842dda381b5a48a
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 3aa89ee..c1bb55c 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -893,6 +893,7 @@
],
static_libs: [
"RoboTestLibraries",
+ "androidx.compose.runtime_runtime",
],
libs: [
"android.test.runner",
@@ -929,6 +930,7 @@
],
static_libs: [
"RoboTestLibraries",
+ "androidx.compose.runtime_runtime",
],
libs: [
"android.test.runner",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
index 976dc52..7d57220 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
@@ -17,8 +17,14 @@
package com.android.systemui.lifecycle
import android.view.View
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -26,6 +32,8 @@
import com.android.systemui.util.Assert
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -149,6 +157,48 @@
assertThat(viewModel.isActivated).isTrue()
}
+
+ @Test
+ fun hydratedStateOf() {
+ val keepAliveMutable = mutableStateOf(true)
+ val upstreamStateFlow = MutableStateFlow(true)
+ val upstreamFlow = upstreamStateFlow.map { !it }
+ composeRule.setContent {
+ val keepAlive by keepAliveMutable
+ if (keepAlive) {
+ val viewModel = rememberViewModel {
+ FakeSysUiViewModel(
+ upstreamFlow = upstreamFlow,
+ upstreamStateFlow = upstreamStateFlow,
+ )
+ }
+
+ Column {
+ Text(
+ "upstreamStateFlow=${viewModel.stateBackedByStateFlow}",
+ Modifier.testTag("upstreamStateFlow")
+ )
+ Text(
+ "upstreamFlow=${viewModel.stateBackedByFlow}",
+ Modifier.testTag("upstreamFlow")
+ )
+ }
+ }
+ }
+
+ composeRule.waitForIdle()
+ composeRule
+ .onNode(hasTestTag("upstreamStateFlow"))
+ .assertTextEquals("upstreamStateFlow=true")
+ composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=false")
+
+ composeRule.runOnUiThread { upstreamStateFlow.value = false }
+ composeRule.waitForIdle()
+ composeRule
+ .onNode(hasTestTag("upstreamStateFlow"))
+ .assertTextEquals("upstreamStateFlow=false")
+ composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=true")
+ }
}
private class FakeViewModel : SysUiViewModel() {
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 104b076..32c4760 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -18,13 +18,45 @@
import android.view.View
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.snapshots.StateFactoryMarker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/** Base class for all System UI view-models. */
abstract class SysUiViewModel : BaseActivatable() {
+ @StateFactoryMarker
+ fun <T> hydratedStateOf(
+ source: StateFlow<T>,
+ ): State<T> {
+ return hydratedStateOf(
+ initialValue = source.value,
+ source = source,
+ )
+ }
+
+ @StateFactoryMarker
+ fun <T> hydratedStateOf(
+ initialValue: T,
+ source: Flow<T>,
+ ): State<T> {
+ val mutableState = mutableStateOf(initialValue)
+ addChild(
+ object : BaseActivatable() {
+ override suspend fun onActivated(): Nothing {
+ source.collect { mutableState.value = it }
+ awaitCancellation()
+ }
+ }
+ )
+ return mutableState
+ }
+
override suspend fun onActivated(): Nothing {
awaitCancellation()
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
index c0bb9a6..90cd8c7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
@@ -16,15 +16,27 @@
package com.android.systemui.lifecycle
+import androidx.compose.runtime.getValue
import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOf
class FakeSysUiViewModel(
private val onActivation: () -> Unit = {},
private val onDeactivation: () -> Unit = {},
+ private val upstreamFlow: Flow<Boolean> = flowOf(true),
+ private val upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(),
) : SysUiViewModel() {
+
var activationCount = 0
var cancellationCount = 0
+ val stateBackedByFlow: Boolean by hydratedStateOf(initialValue = true, source = upstreamFlow)
+ val stateBackedByStateFlow: Boolean by hydratedStateOf(source = upstreamStateFlow)
+
override suspend fun onActivated(): Nothing {
activationCount++
onActivation()