Loading packages/SystemUI/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -893,6 +893,7 @@ android_robolectric_test { ], static_libs: [ "RoboTestLibraries", "androidx.compose.runtime_runtime", ], libs: [ "android.test.runner", Loading Loading @@ -929,6 +930,7 @@ android_robolectric_test { ], static_libs: [ "RoboTestLibraries", "androidx.compose.runtime_runtime", ], libs: [ "android.test.runner", Loading packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +50 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -26,6 +32,8 @@ import com.android.systemui.SysuiTestCase 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 Loading Loading @@ -149,6 +157,48 @@ class SysUiViewModelTest : SysuiTestCase() { 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() { Loading packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +32 −0 Original line number Diff line number Diff line Loading @@ -18,13 +18,45 @@ package com.android.systemui.lifecycle 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() } Loading packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt +12 −0 Original line number Diff line number Diff line Loading @@ -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() Loading Loading
packages/SystemUI/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -893,6 +893,7 @@ android_robolectric_test { ], static_libs: [ "RoboTestLibraries", "androidx.compose.runtime_runtime", ], libs: [ "android.test.runner", Loading Loading @@ -929,6 +930,7 @@ android_robolectric_test { ], static_libs: [ "RoboTestLibraries", "androidx.compose.runtime_runtime", ], libs: [ "android.test.runner", Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +50 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -26,6 +32,8 @@ import com.android.systemui.SysuiTestCase 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 Loading Loading @@ -149,6 +157,48 @@ class SysUiViewModelTest : SysuiTestCase() { 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() { Loading
packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +32 −0 Original line number Diff line number Diff line Loading @@ -18,13 +18,45 @@ package com.android.systemui.lifecycle 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() } Loading
packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt +12 −0 Original line number Diff line number Diff line Loading @@ -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() Loading