Loading packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt→packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +79 −1 Original line number Diff line number Diff line Loading @@ -17,15 +17,21 @@ package com.android.systemui.lifecycle import android.view.View import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.ui.viewmodel.FakeSysUiViewModel import com.android.systemui.util.Assert import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent Loading Loading @@ -79,7 +85,7 @@ class SysUiViewModelTest : SysuiTestCase() { // return Unit instead of FakeSysUiViewModel. It might be an issue with the compose // compiler. val unused: FakeSysUiViewModel = rememberViewModel("test", key) { rememberViewModel("test", key = key) { when (key) { 1 -> FakeSysUiViewModel( Loading Loading @@ -109,6 +115,78 @@ class SysUiViewModelTest : SysuiTestCase() { assertThat(isActive2).isFalse() } @Test fun rememberActivated_minActiveState_CREATED() { assertActivationThroughAllLifecycleStates(Lifecycle.State.CREATED) } @Test fun rememberActivated_minActiveState_STARTED() { assertActivationThroughAllLifecycleStates(Lifecycle.State.STARTED) } @Test fun rememberActivated_minActiveState_RESUMED() { assertActivationThroughAllLifecycleStates(Lifecycle.State.RESUMED) } private fun assertActivationThroughAllLifecycleStates(minActiveState: Lifecycle.State) { var isActive = false val lifecycleOwner = composeRule.runOnUiThread { object : LifecycleOwner { override val lifecycle = LifecycleRegistry(this) init { lifecycle.currentState = Lifecycle.State.CREATED } } } composeRule.setContent { CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { // Need to explicitly state the type to avoid a weird issue where the factory seems // to return Unit instead of FakeSysUiViewModel. It might be an issue with the // compose compiler. val unused: FakeSysUiViewModel = rememberViewModel(traceName = "test", minActiveState = minActiveState) { FakeSysUiViewModel( onActivation = { isActive = true }, onDeactivation = { isActive = false }, ) } } } // Increase state, step-by-step, all the way to RESUMED, the maximum state and then, reverse // course and decrease the state, step-by-step, all the way back down to CREATED. Lastly, // move to DESTROYED to finish up. // // In each step along the way, verify that our Activatable is active or not, based on the // minActiveState that we received. The Activatable should be active only if the current\ // lifecycle state is equal to or "greater" than the minActiveState. listOf( Lifecycle.State.CREATED, Lifecycle.State.STARTED, Lifecycle.State.RESUMED, Lifecycle.State.STARTED, Lifecycle.State.CREATED, Lifecycle.State.DESTROYED, ) .forEachIndexed { index, lifecycleState -> composeRule.runOnUiThread { lifecycleOwner.lifecycle.currentState = lifecycleState } composeRule.waitForIdle() val expectedIsActive = lifecycleState.isAtLeast(minActiveState) assertWithMessage( "isActive=$isActive but expected to be $expectedIsActive when" + " lifecycleState=$lifecycleState because $lifecycleState is" + " ${if (expectedIsActive) "equal to or greater" else "less"} than" + " minActiveState=$minActiveState (iteration #$index)" ) .that(isActive) .isEqualTo(expectedIsActive) } } @Test fun rememberActivated_leavingTheComposition() { val keepAliveMutable = mutableStateOf(true) Loading packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +15 −3 Original line number Diff line number Diff line Loading @@ -18,10 +18,11 @@ package com.android.systemui.lifecycle import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.lifecycle.Lifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.coroutines.traceCoroutine import com.android.compose.lifecycle.LaunchedEffectWithLifecycle import kotlinx.coroutines.CoroutineScope /** Loading @@ -34,12 +35,23 @@ import kotlinx.coroutines.CoroutineScope * that's unique enough and easy enough to find in code search; this should help correlate * performance findings with actual code. One recommendation: prefer whole string literals instead * of some complex concatenation or templating scheme. * * The remembered view-model is activated every time the [minActiveState] is reached and deactivated * each time the lifecycle state falls "below" the [minActiveState]. This can be used to have more * granular control over when exactly a view-model becomes active. */ @Composable fun <T> rememberViewModel(traceName: String, key: Any = Unit, factory: () -> T): T { fun <T> rememberViewModel( traceName: String, minActiveState: Lifecycle.State = Lifecycle.State.STARTED, key: Any = Unit, factory: () -> T, ): T { val instance = remember(key) { factory() } if (instance is Activatable) { LaunchedEffect(instance) { traceCoroutine(traceName) { instance.activate() } } LaunchedEffectWithLifecycle(key1 = instance, minActiveState = minActiveState) { traceCoroutine(traceName) { instance.activate() } } } return instance } Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +1 −1 Original line number Diff line number Diff line Loading @@ -86,7 +86,7 @@ constructor( val context = LocalContext.current val textFeedbackViewModel = rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", context) { rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", key = context) { textFeedbackContentViewModelFactory.create(context) } Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +1 −1 Original line number Diff line number Diff line Loading @@ -181,7 +181,7 @@ private fun ToolbarTextFeedback( Box(modifier = modifier) { val context = LocalContext.current val viewModel = rememberViewModel("Toolbar.TextFeedbackViewModel", context) { rememberViewModel("Toolbar.TextFeedbackViewModel", key = context) { viewModelFactory.create(context) } val hasTextFeedback = viewModel.textFeedback !is TextFeedbackViewModel.NoFeedback Loading Loading
packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt→packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +79 −1 Original line number Diff line number Diff line Loading @@ -17,15 +17,21 @@ package com.android.systemui.lifecycle import android.view.View import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.ui.viewmodel.FakeSysUiViewModel import com.android.systemui.util.Assert import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent Loading Loading @@ -79,7 +85,7 @@ class SysUiViewModelTest : SysuiTestCase() { // return Unit instead of FakeSysUiViewModel. It might be an issue with the compose // compiler. val unused: FakeSysUiViewModel = rememberViewModel("test", key) { rememberViewModel("test", key = key) { when (key) { 1 -> FakeSysUiViewModel( Loading Loading @@ -109,6 +115,78 @@ class SysUiViewModelTest : SysuiTestCase() { assertThat(isActive2).isFalse() } @Test fun rememberActivated_minActiveState_CREATED() { assertActivationThroughAllLifecycleStates(Lifecycle.State.CREATED) } @Test fun rememberActivated_minActiveState_STARTED() { assertActivationThroughAllLifecycleStates(Lifecycle.State.STARTED) } @Test fun rememberActivated_minActiveState_RESUMED() { assertActivationThroughAllLifecycleStates(Lifecycle.State.RESUMED) } private fun assertActivationThroughAllLifecycleStates(minActiveState: Lifecycle.State) { var isActive = false val lifecycleOwner = composeRule.runOnUiThread { object : LifecycleOwner { override val lifecycle = LifecycleRegistry(this) init { lifecycle.currentState = Lifecycle.State.CREATED } } } composeRule.setContent { CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { // Need to explicitly state the type to avoid a weird issue where the factory seems // to return Unit instead of FakeSysUiViewModel. It might be an issue with the // compose compiler. val unused: FakeSysUiViewModel = rememberViewModel(traceName = "test", minActiveState = minActiveState) { FakeSysUiViewModel( onActivation = { isActive = true }, onDeactivation = { isActive = false }, ) } } } // Increase state, step-by-step, all the way to RESUMED, the maximum state and then, reverse // course and decrease the state, step-by-step, all the way back down to CREATED. Lastly, // move to DESTROYED to finish up. // // In each step along the way, verify that our Activatable is active or not, based on the // minActiveState that we received. The Activatable should be active only if the current\ // lifecycle state is equal to or "greater" than the minActiveState. listOf( Lifecycle.State.CREATED, Lifecycle.State.STARTED, Lifecycle.State.RESUMED, Lifecycle.State.STARTED, Lifecycle.State.CREATED, Lifecycle.State.DESTROYED, ) .forEachIndexed { index, lifecycleState -> composeRule.runOnUiThread { lifecycleOwner.lifecycle.currentState = lifecycleState } composeRule.waitForIdle() val expectedIsActive = lifecycleState.isAtLeast(minActiveState) assertWithMessage( "isActive=$isActive but expected to be $expectedIsActive when" + " lifecycleState=$lifecycleState because $lifecycleState is" + " ${if (expectedIsActive) "equal to or greater" else "less"} than" + " minActiveState=$minActiveState (iteration #$index)" ) .that(isActive) .isEqualTo(expectedIsActive) } } @Test fun rememberActivated_leavingTheComposition() { val keepAliveMutable = mutableStateOf(true) Loading
packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +15 −3 Original line number Diff line number Diff line Loading @@ -18,10 +18,11 @@ package com.android.systemui.lifecycle import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.lifecycle.Lifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.coroutines.traceCoroutine import com.android.compose.lifecycle.LaunchedEffectWithLifecycle import kotlinx.coroutines.CoroutineScope /** Loading @@ -34,12 +35,23 @@ import kotlinx.coroutines.CoroutineScope * that's unique enough and easy enough to find in code search; this should help correlate * performance findings with actual code. One recommendation: prefer whole string literals instead * of some complex concatenation or templating scheme. * * The remembered view-model is activated every time the [minActiveState] is reached and deactivated * each time the lifecycle state falls "below" the [minActiveState]. This can be used to have more * granular control over when exactly a view-model becomes active. */ @Composable fun <T> rememberViewModel(traceName: String, key: Any = Unit, factory: () -> T): T { fun <T> rememberViewModel( traceName: String, minActiveState: Lifecycle.State = Lifecycle.State.STARTED, key: Any = Unit, factory: () -> T, ): T { val instance = remember(key) { factory() } if (instance is Activatable) { LaunchedEffect(instance) { traceCoroutine(traceName) { instance.activate() } } LaunchedEffectWithLifecycle(key1 = instance, minActiveState = minActiveState) { traceCoroutine(traceName) { instance.activate() } } } return instance } Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +1 −1 Original line number Diff line number Diff line Loading @@ -86,7 +86,7 @@ constructor( val context = LocalContext.current val textFeedbackViewModel = rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", context) { rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", key = context) { textFeedbackContentViewModelFactory.create(context) } Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +1 −1 Original line number Diff line number Diff line Loading @@ -181,7 +181,7 @@ private fun ToolbarTextFeedback( Box(modifier = modifier) { val context = LocalContext.current val viewModel = rememberViewModel("Toolbar.TextFeedbackViewModel", context) { rememberViewModel("Toolbar.TextFeedbackViewModel", key = context) { viewModelFactory.create(context) } val hasTextFeedback = viewModel.textFeedback !is TextFeedbackViewModel.NoFeedback Loading