Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8cbb37d4 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] rememberViewModel uses lifecycle" into main

parents 17c394b2 6674924c
Loading
Loading
Loading
Loading
+79 −1
Original line number Diff line number Diff line
@@ -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
@@ -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(
@@ -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)
+15 −3
Original line number Diff line number Diff line
@@ -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

/**
@@ -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
}
+1 −1
Original line number Diff line number Diff line
@@ -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)
            }

+1 −1
Original line number Diff line number Diff line
@@ -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