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

Commit 4241d874 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

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
parent 465342be
Loading
Loading
Loading
Loading
+2 −0
Original line number Original line Diff line number Diff line
@@ -893,6 +893,7 @@ android_robolectric_test {
    ],
    ],
    static_libs: [
    static_libs: [
        "RoboTestLibraries",
        "RoboTestLibraries",
        "androidx.compose.runtime_runtime",
    ],
    ],
    libs: [
    libs: [
        "android.test.runner",
        "android.test.runner",
@@ -929,6 +930,7 @@ android_robolectric_test {
    ],
    ],
    static_libs: [
    static_libs: [
        "RoboTestLibraries",
        "RoboTestLibraries",
        "androidx.compose.runtime_runtime",
    ],
    ],
    libs: [
    libs: [
        "android.test.runner",
        "android.test.runner",
+50 −0
Original line number Original line Diff line number Diff line
@@ -17,8 +17,14 @@
package com.android.systemui.lifecycle
package com.android.systemui.lifecycle


import android.view.View
import android.view.View
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
@@ -26,6 +32,8 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.util.Assert
import com.android.systemui.util.Assert
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.runTest
@@ -149,6 +157,48 @@ class SysUiViewModelTest : SysuiTestCase() {


        assertThat(viewModel.isActivated).isTrue()
        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() {
private class FakeViewModel : SysUiViewModel() {
+32 −0
Original line number Original line Diff line number Diff line
@@ -18,13 +18,45 @@ package com.android.systemui.lifecycle


import android.view.View
import android.view.View
import androidx.compose.runtime.Composable
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.CoroutineScope
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch


/** Base class for all System UI view-models. */
/** Base class for all System UI view-models. */
abstract class SysUiViewModel : BaseActivatable() {
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 {
    override suspend fun onActivated(): Nothing {
        awaitCancellation()
        awaitCancellation()
    }
    }
+12 −0
Original line number Original line Diff line number Diff line
@@ -16,15 +16,27 @@


package com.android.systemui.lifecycle
package com.android.systemui.lifecycle


import androidx.compose.runtime.getValue
import kotlinx.coroutines.awaitCancellation
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(
class FakeSysUiViewModel(
    private val onActivation: () -> Unit = {},
    private val onActivation: () -> Unit = {},
    private val onDeactivation: () -> Unit = {},
    private val onDeactivation: () -> Unit = {},
    private val upstreamFlow: Flow<Boolean> = flowOf(true),
    private val upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(),
) : SysUiViewModel() {
) : SysUiViewModel() {

    var activationCount = 0
    var activationCount = 0
    var cancellationCount = 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 {
    override suspend fun onActivated(): Nothing {
        activationCount++
        activationCount++
        onActivation()
        onActivation()