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 Diff line number Diff line
@@ -893,6 +893,7 @@ android_robolectric_test {
    ],
    static_libs: [
        "RoboTestLibraries",
        "androidx.compose.runtime_runtime",
    ],
    libs: [
        "android.test.runner",
@@ -929,6 +930,7 @@ android_robolectric_test {
    ],
    static_libs: [
        "RoboTestLibraries",
        "androidx.compose.runtime_runtime",
    ],
    libs: [
        "android.test.runner",
+50 −0
Original line number Diff line number Diff line
@@ -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.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
@@ -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() {
+32 −0
Original line number Diff line number Diff line
@@ -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()
    }
+12 −0
Original line number Diff line number Diff line
@@ -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()