diff --git a/app/src/main/java/com/aurora/store/compose/composition/FlowExtensions.kt b/app/src/main/java/com/aurora/store/compose/composition/FlowExtensions.kt new file mode 100644 index 000000000..e14f8a653 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composition/FlowExtensions.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composition + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy +import androidx.compose.runtime.remember +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** + * Collects a [StateFlow] as [State], always triggering recomposition on every emission + * regardless of structural equality, by using [neverEqualPolicy]. + */ +@Composable +internal fun StateFlow.collectForced(): State { + val lifecycleOwner = LocalLifecycleOwner.current + val state: MutableState = remember(this) { mutableStateOf(value, neverEqualPolicy()) } + LaunchedEffect(this, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + collect { state.value = it } + } + } + return state +} + +/** + * Collects a [Flow] as [State] starting from [initial], always triggering recomposition on + * every emission regardless of structural equality, by using [neverEqualPolicy]. + * + * Useful for non-state flows (e.g. [kotlinx.coroutines.flow.SharedFlow]) and for flows whose + * values have broken `equals` implementations that would otherwise be conflated upstream. + */ +@Composable +internal fun Flow.collectForced(initial: T): State { + val lifecycleOwner = LocalLifecycleOwner.current + val state: MutableState = remember(this) { mutableStateOf(initial, neverEqualPolicy()) } + LaunchedEffect(this, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + collect { state.value = it } + } + } + return state +} + +/** + * Observes a [LiveData] as [State], always triggering recomposition on every emission + * regardless of structural equality, by using [neverEqualPolicy]. + */ +@Suppress("UNCHECKED_CAST") +@Composable +internal fun LiveData.observeForced(): State { + val lifecycleOwner = LocalLifecycleOwner.current + val state: MutableState = remember { mutableStateOf(value, neverEqualPolicy()) } + DisposableEffect(this, lifecycleOwner) { + val observer = Observer { state.value = it } + observe(lifecycleOwner, observer) + onDispose { removeObserver(observer) } + } + return state +}