refactor(ui): Replace waitForState and shareLatest with lifecycle-aware state collection

Add collectAsStateWithLifecycle for all Compose state collection (except Glance widgets). Add asLiveState() extension to ViewModel2 as a simpler replacement for shareLatest(scope = vmScope). Remove custom waitForState helper and shareLatest utility.
This commit is contained in:
darken
2026-03-03 17:21:55 +00:00
committed by Matthias Urhahn
parent e13331dfcf
commit cd6488a713
28 changed files with 63 additions and 94 deletions
-2
View File
@@ -24,8 +24,6 @@ PR titles appear in auto-generated changelogs and are read by users. Use **ELI5,
| **General** | Dashboard, settings, notifications, themes, onboarding, support, app-wide UI | | **General** | Dashboard, settings, notifications, themes, onboarding, support, app-wide UI |
| **Fix** | Bug fixes spanning multiple areas | | **Fix** | Bug fixes spanning multiple areas |
For non-user-facing PRs (refactors, tests, CI, dependency bumps, translations), apply the `changelog-ignore` GitHub label. These don't need user-facing titles.
### Title Examples ### Title Examples
- `Widget: Add color themes and transparency slider` - `Widget: Add color themes and transparency slider`
@@ -1,13 +0,0 @@
package eu.darken.capod.common.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import kotlinx.coroutines.flow.Flow
@Composable
fun <T> waitForState(flow: Flow<T>): State<T?> {
return produceState(initialValue = null) {
flow.collect { value = it }
}
}
@@ -9,13 +9,12 @@ import androidx.activity.viewModels
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.common.compose.waitForState
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.theming.CapodTheme import eu.darken.capod.common.theming.CapodTheme
import eu.darken.capod.common.uix.Activity2 import eu.darken.capod.common.uix.Activity2
@@ -41,7 +40,7 @@ class RecorderActivity : Activity2() {
} }
setContent { setContent {
val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) val themeState by generalSettings.themeState.collectAsStateWithLifecycle(initialValue = generalSettings.currentThemeState)
CapodTheme(state = themeState) { CapodTheme(state = themeState) {
val backgroundColor = MaterialTheme.colorScheme.background val backgroundColor = MaterialTheme.colorScheme.background
val useDarkIcons = backgroundColor.luminance() > 0.5f val useDarkIcons = backgroundColor.luminance() > 0.5f
@@ -61,7 +60,7 @@ class RecorderActivity : Activity2() {
} }
} }
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { state?.let {
RecorderScreen( RecorderScreen(
state = it, state = it,
@@ -14,41 +14,17 @@ import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.transformWhile
import kotlin.time.Duration import kotlin.time.Duration
/**
* Create a stateful flow, with the initial value of null, but never emits a null value.
* Helper method to create a new flow without suspending and without initial value
* The flow collector will just wait for the first value
*/
fun <T : Any> Flow<T>.shareLatest(
tag: String? = null,
scope: CoroutineScope,
started: SharingStarted = SharingStarted.WhileSubscribed(replayExpirationMillis = 0)
) = this
.onStart { if (tag != null) log(tag) { "shareLatest(...) start" } }
.onEach { if (tag != null) log(tag) { "shareLatest(...) emission: $it" } }
.onCompletion { if (tag != null) log(tag) { "shareLatest(...) completed." } }
.catch {
if (tag != null) log(tag) { "shareLatest(...) catch(): ${it.asLog()}" }
throw it
}
.stateIn(
scope = scope,
started = started,
initialValue = null
)
.filterNotNull()
fun <T : Any?> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn( fun <T : Any?> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
scope = scope, scope = scope,
@@ -13,7 +13,11 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@@ -64,4 +68,12 @@ abstract class ViewModel2(
open fun <T> Flow<T>.launchInViewModel() = this.launchIn(vmScope) open fun <T> Flow<T>.launchInViewModel() = this.launchIn(vmScope)
fun <T : Any> Flow<T>.asLiveState(
started: SharingStarted = SharingStarted.WhileSubscribed(5_000),
): Flow<T> = this.stateIn(
scope = vmScope,
started = started,
initialValue = null,
).filterNotNull()
} }
@@ -6,8 +6,8 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -49,7 +49,7 @@ class MainActivity : Activity2() {
} }
setContent { setContent {
val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) val themeState by generalSettings.themeState.collectAsStateWithLifecycle(initialValue = generalSettings.currentThemeState)
val backStack = rememberNavBackStack(startDestination) val backStack = rememberNavBackStack(startDestination)
navCtrl.setup(backStack) navCtrl.setup(backStack)
@@ -26,7 +26,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.waitForState
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -43,7 +43,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider import eu.darken.capod.common.compose.preview.MockPodDataProvider
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.permissions.Permission
@@ -129,11 +129,11 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) {
} }
} }
val stateHolder = waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
val state = stateHolder.value ?: return val currentState = state ?: return
OverviewScreen( OverviewScreen(
state = state, state = currentState,
onRequestPermission = { vm.requestPermission(it) }, onRequestPermission = { vm.requestPermission(it) },
onManageDevices = { vm.goToDeviceManager() }, onManageDevices = { vm.goToDeviceManager() },
onSettings = { vm.goToSettings() }, onSettings = { vm.goToSettings() },
@@ -9,7 +9,7 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.SingleEventFlow import eu.darken.capod.common.flow.SingleEventFlow
import eu.darken.capod.common.flow.combine import eu.darken.capod.common.flow.combine
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.flow.throttleLatest import eu.darken.capod.common.flow.throttleLatest
import eu.darken.capod.common.navigation.Nav import eu.darken.capod.common.navigation.Nav
import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.permissions.Permission
@@ -74,7 +74,7 @@ class OverviewViewModel @Inject constructor(
monitorControl.startMonitor() monitorControl.startMonitor()
} }
} }
.shareLatest(scope = vmScope) .asLiveState()
private val updateTicker = channelFlow<Unit> { private val updateTicker = channelFlow<Unit> {
while (isActive) { while (isActive) {
@@ -113,7 +113,7 @@ class OverviewViewModel @Inject constructor(
upgradeInfo = upgradeInfo, upgradeInfo = upgradeInfo,
showUnmatchedDevices = showUnmatched, showUnmatchedDevices = showUnmatched,
) )
}.shareLatest(scope = vmScope) }.asLiveState()
data class State( data class State(
val now: Instant, val now: Instant,
@@ -29,7 +29,7 @@ import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.BuildConfigWrap import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.PrivacyPolicy import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.Nav import eu.darken.capod.common.navigation.Nav
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -41,7 +41,7 @@ fun SettingsScreenHost(vm: SettingsViewModel = hiltViewModel()) {
ErrorEventHandler(vm) ErrorEventHandler(vm)
NavigationEventHandler(vm) NavigationEventHandler(vm)
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { state?.let {
SettingsScreen( SettingsScreen(
state = it, state = it,
@@ -4,7 +4,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.WebpageTool import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.common.upgrade.UpgradeRepo import eu.darken.capod.common.upgrade.UpgradeRepo
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -23,7 +23,7 @@ class SettingsViewModel @Inject constructor(
val state = upgradeRepo.upgradeInfo val state = upgradeRepo.upgradeInfo
.map { State(sponsorUrl = upgradeRepo.getSponsorUrl()) } .map { State(sponsorUrl = upgradeRepo.getSponsorUrl()) }
.shareLatest(scope = vmScope) .asLiveState()
fun openUrl(url: String) { fun openUrl(url: String) {
webpageTool.open(url) webpageTool.open(url)
@@ -51,7 +51,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.bluetooth.ScannerMode import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.settings.SettingsBaseItem import eu.darken.capod.common.settings.SettingsBaseItem
@@ -74,7 +74,7 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) {
vm.launchUpgradeFlow.collect { action -> activity?.let { action(it) } } vm.launchUpgradeFlow.collect { action -> activity?.let { action(it) } }
} }
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { state?.let {
GeneralSettingsScreen( GeneralSettingsScreen(
state = it, state = it,
@@ -6,7 +6,6 @@ import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.SingleEventFlow import eu.darken.capod.common.flow.SingleEventFlow
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.navigation.Nav import eu.darken.capod.common.navigation.Nav
import eu.darken.capod.common.theming.ThemeColor import eu.darken.capod.common.theming.ThemeColor
import eu.darken.capod.common.theming.ThemeMode import eu.darken.capod.common.theming.ThemeMode
@@ -41,7 +40,7 @@ class GeneralSettingsViewModel @Inject constructor(
val themeState: ThemeState, val themeState: ThemeState,
) )
private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.shareLatest(scope = vmScope) private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveState()
val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>() val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>()
@@ -77,7 +76,7 @@ class GeneralSettingsViewModel @Inject constructor(
useIndirectScanResultCallback = compat[2] as Boolean, useIndirectScanResultCallback = compat[2] as Boolean,
themeState = themeState, themeState = themeState,
) )
}.shareLatest(scope = vmScope) }.asLiveState()
fun setMonitorMode(mode: MonitorMode) { fun setMonitorMode(mode: MonitorMode) {
generalSettings.monitorMode.value = mode generalSettings.monitorMode.value = mode
@@ -23,7 +23,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.settings.SettingsBaseItem import eu.darken.capod.common.settings.SettingsBaseItem
@@ -33,7 +33,7 @@ fun DebugSettingsScreenHost(vm: DebugSettingsViewModel = hiltViewModel()) {
ErrorEventHandler(vm) ErrorEventHandler(vm)
NavigationEventHandler(vm) NavigationEventHandler(vm)
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { state?.let {
DebugSettingsScreen( DebugSettingsScreen(
state = it, state = it,
@@ -4,7 +4,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.DebugSettings import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.common.uix.ViewModel4
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import javax.inject.Inject import javax.inject.Inject
@@ -31,7 +31,7 @@ class DebugSettingsViewModel @Inject constructor(
showFakeData = fakeData, showFakeData = fakeData,
showUnfiltered = unfiltered, showUnfiltered = unfiltered,
) )
}.shareLatest(scope = vmScope) }.asLiveState()
fun setDebugModeEnabled(enabled: Boolean) { fun setDebugModeEnabled(enabled: Boolean) {
debugSettings.isDebugModeEnabled.value = enabled debugSettings.isDebugModeEnabled.value = enabled
@@ -33,7 +33,7 @@ import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.debug.recording.ui.RecorderConsentDialog import eu.darken.capod.common.debug.recording.ui.RecorderConsentDialog
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -50,7 +50,7 @@ fun SupportScreenHost(vm: SupportViewModel = hiltViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
var showShortRecordingWarning by remember { mutableStateOf(false) } var showShortRecordingWarning by remember { mutableStateOf(false) }
@@ -57,7 +57,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.WebpageTool import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.debug.recording.ui.RecorderConsentDialog import eu.darken.capod.common.debug.recording.ui.RecorderConsentDialog
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -120,7 +120,7 @@ fun ContactFormScreenHost(vm: ContactFormViewModel = hiltViewModel()) {
} }
} }
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { state?.let {
ContactFormScreen( ContactFormScreen(
state = it, state = it,
@@ -9,8 +9,8 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@@ -18,7 +18,6 @@ import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.compose.waitForState
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.theming.CapodTheme import eu.darken.capod.common.theming.CapodTheme
@@ -61,7 +60,7 @@ class WidgetConfigurationActivity : Activity2() {
} }
setContent { setContent {
val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) val themeState by generalSettings.themeState.collectAsStateWithLifecycle(initialValue = generalSettings.currentThemeState)
CapodTheme(state = themeState) { CapodTheme(state = themeState) {
val backgroundColor = MaterialTheme.colorScheme.background val backgroundColor = MaterialTheme.colorScheme.background
val useDarkIcons = backgroundColor.luminance() > 0.5f val useDarkIcons = backgroundColor.luminance() > 0.5f
@@ -72,7 +71,7 @@ class WidgetConfigurationActivity : Activity2() {
insetsController.isAppearanceLightNavigationBars = useDarkIcons insetsController.isAppearanceLightNavigationBars = useDarkIcons
} }
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { currentState -> state?.let { currentState ->
WidgetConfigurationScreen( WidgetConfigurationScreen(
state = currentState, state = currentState,
@@ -9,7 +9,6 @@ import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.combine import eu.darken.capod.common.flow.combine
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.uix.ViewModel2 import eu.darken.capod.common.uix.ViewModel2
import eu.darken.capod.common.upgrade.UpgradeRepo import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.profiles.core.DeviceProfile import eu.darken.capod.profiles.core.DeviceProfile
@@ -70,7 +69,7 @@ class WidgetConfigurationViewModel @Inject constructor(
activePreset = activePreset, activePreset = activePreset,
isCustomMode = activePreset == null, isCustomMode = activePreset == null,
) )
}.shareLatest(scope = vmScope) }.asLiveState()
data class State( data class State(
val profiles: List<DeviceProfile> = emptyList(), val profiles: List<DeviceProfile> = emptyList(),
@@ -39,7 +39,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider import eu.darken.capod.common.compose.preview.MockPodDataProvider
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
@@ -50,7 +50,7 @@ fun DeviceManagerScreenHost(vm: DeviceManagerViewModel = hiltViewModel()) {
ErrorEventHandler(vm) ErrorEventHandler(vm)
NavigationEventHandler(vm) NavigationEventHandler(vm)
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { state?.let {
DeviceManagerScreen( DeviceManagerScreen(
state = it, state = it,
@@ -4,7 +4,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.navigation.Nav import eu.darken.capod.common.navigation.Nav
import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.profiles.core.DeviceProfile import eu.darken.capod.profiles.core.DeviceProfile
@@ -20,7 +20,7 @@ class DeviceManagerViewModel @Inject constructor(
val state = deviceProfilesRepo.profiles.map { profiles -> val state = deviceProfilesRepo.profiles.map { profiles ->
State(profiles = profiles) State(profiles = profiles)
}.shareLatest(scope = vmScope) }.asLiveState()
data class State( data class State(
val profiles: List<DeviceProfile>, val profiles: List<DeviceProfile>,
@@ -61,7 +61,7 @@ import eu.darken.capod.R
import eu.darken.capod.common.bluetooth.BluetoothDevice2 import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.toHex import eu.darken.capod.common.toHex
@@ -77,7 +77,7 @@ fun DeviceProfileCreationScreenHost(
ErrorEventHandler(vm) ErrorEventHandler(vm)
NavigationEventHandler(vm) NavigationEventHandler(vm)
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
var showUnsavedChangesDialog by rememberSaveable { mutableStateOf(false) } var showUnsavedChangesDialog by rememberSaveable { mutableStateOf(false) }
var showDeleteDialog by rememberSaveable { mutableStateOf(false) } var showDeleteDialog by rememberSaveable { mutableStateOf(false) }
@@ -11,7 +11,6 @@ import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.SingleEventFlow import eu.darken.capod.common.flow.SingleEventFlow
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.fromHex import eu.darken.capod.common.fromHex
import eu.darken.capod.common.toHex import eu.darken.capod.common.toHex
import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.common.uix.ViewModel4
@@ -118,7 +117,7 @@ class DeviceProfileCreationViewModel @Inject constructor(
minimumSignalQuality = editorState.minimumSignalQuality, minimumSignalQuality = editorState.minimumSignalQuality,
canSave = formValid && hasChanges, canSave = formValid && hasChanges,
) )
}.shareLatest(scope = vmScope) }.asLiveState()
data class State( data class State(
val isEditMode: Boolean, val isEditMode: Boolean,
@@ -43,7 +43,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.settings.SettingsBaseItem import eu.darken.capod.common.settings.SettingsBaseItem
@@ -55,7 +55,7 @@ fun ReactionSettingsScreenHost(vm: ReactionSettingsViewModel = hiltViewModel())
ErrorEventHandler(vm) ErrorEventHandler(vm)
NavigationEventHandler(vm) NavigationEventHandler(vm)
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
val activity = LocalContext.current as Activity val activity = LocalContext.current as Activity
state?.let { state?.let {
@@ -4,7 +4,7 @@ import android.app.Activity
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.common.upgrade.UpgradeRepo import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.GeneralSettings
@@ -35,7 +35,7 @@ class ReactionSettingsViewModel @Inject constructor(
val showPopUpOnConnection: Boolean, val showPopUpOnConnection: Boolean,
) )
private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.shareLatest(scope = vmScope) private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveState()
val state = combine( val state = combine(
isPro, isPro,
@@ -57,7 +57,7 @@ class ReactionSettingsViewModel @Inject constructor(
showPopUpOnCaseOpen = values[6] as Boolean, showPopUpOnCaseOpen = values[6] as Boolean,
showPopUpOnConnection = values[7] as Boolean, showPopUpOnConnection = values[7] as Boolean,
) )
}.shareLatest(scope = vmScope) }.asLiveState()
fun setOnePodMode(enabled: Boolean) { fun setOnePodMode(enabled: Boolean) {
reactionSettings.onePodMode.value = enabled reactionSettings.onePodMode.value = enabled
@@ -32,7 +32,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.waitForState import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -41,7 +41,7 @@ fun TroubleShooterScreenHost(vm: TroubleShooterViewModel = hiltViewModel()) {
ErrorEventHandler(vm) ErrorEventHandler(vm)
NavigationEventHandler(vm) NavigationEventHandler(vm)
val state by waitForState(vm.state) val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
state?.let { state?.let {
TroubleShooterScreen( TroubleShooterScreen(
state = it, state = it,
@@ -10,7 +10,7 @@ import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.shareLatest
import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.PodMonitor
@@ -45,7 +45,7 @@ class TroubleShooterViewModel @Inject constructor(
data class State(val bleState: BleState) data class State(val bleState: BleState)
val state = _bleState.map { State(it) }.shareLatest(scope = vmScope) val state = _bleState.map { State(it) }.asLiveState()
init { init {
_bleState _bleState
+1
View File
@@ -112,6 +112,7 @@ fun DependencyHandlerScope.addCompose() {
implementation("androidx.activity:activity-compose:1.12.1") implementation("androidx.activity:activity-compose:1.12.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
implementation("androidx.hilt:hilt-navigation-compose:1.3.0-alpha01") implementation("androidx.hilt:hilt-navigation-compose:1.3.0-alpha01")
} }