diff --git a/.claude/rules/pull-requests.md b/.claude/rules/pull-requests.md index f5889f7d..7cfc9b1c 100644 --- a/.claude/rules/pull-requests.md +++ b/.claude/rules/pull-requests.md @@ -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 | | **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 - `Widget: Add color themes and transparency slider` diff --git a/app/src/main/java/eu/darken/capod/common/compose/ViewModelExtensions.kt b/app/src/main/java/eu/darken/capod/common/compose/ViewModelExtensions.kt deleted file mode 100644 index 663eef1f..00000000 --- a/app/src/main/java/eu/darken/capod/common/compose/ViewModelExtensions.kt +++ /dev/null @@ -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 waitForState(flow: Flow): State { - return produceState(initialValue = null) { - flow.collect { value = it } - } -} diff --git a/app/src/main/java/eu/darken/capod/common/debug/recording/ui/RecorderActivity.kt b/app/src/main/java/eu/darken/capod/common/debug/recording/ui/RecorderActivity.kt index 4b1c8894..4e1f86ba 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/recording/ui/RecorderActivity.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/recording/ui/RecorderActivity.kt @@ -9,13 +9,12 @@ import androidx.activity.viewModels import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.toArgb import androidx.core.view.WindowCompat 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.theming.CapodTheme import eu.darken.capod.common.uix.Activity2 @@ -41,7 +40,7 @@ class RecorderActivity : Activity2() { } setContent { - val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) + val themeState by generalSettings.themeState.collectAsStateWithLifecycle(initialValue = generalSettings.currentThemeState) CapodTheme(state = themeState) { val backgroundColor = MaterialTheme.colorScheme.background 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 { RecorderScreen( state = it, diff --git a/app/src/main/java/eu/darken/capod/common/flow/FlowExtensions.kt b/app/src/main/java/eu/darken/capod/common/flow/FlowExtensions.kt index 3525403e..77d7ae77 100644 --- a/app/src/main/java/eu/darken/capod/common/flow/FlowExtensions.kt +++ b/app/src/main/java/eu/darken/capod/common/flow/FlowExtensions.kt @@ -14,41 +14,17 @@ import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.stateIn + import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformWhile 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 Flow.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 Flow.replayingShare(scope: CoroutineScope) = this.shareIn( scope = scope, diff --git a/app/src/main/java/eu/darken/capod/common/uix/ViewModel2.kt b/app/src/main/java/eu/darken/capod/common/uix/ViewModel2.kt index 2e469cd3..42ccdcfe 100644 --- a/app/src/main/java/eu/darken/capod/common/uix/ViewModel2.kt +++ b/app/src/main/java/eu/darken/capod/common/uix/ViewModel2.kt @@ -13,7 +13,11 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope 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.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus import kotlin.coroutines.CoroutineContext @@ -64,4 +68,12 @@ abstract class ViewModel2( open fun Flow.launchInViewModel() = this.launchIn(vmScope) + fun Flow.asLiveState( + started: SharingStarted = SharingStarted.WhileSubscribed(5_000), + ): Flow = this.stateIn( + scope = vmScope, + started = started, + initialValue = null, + ).filterNotNull() + } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt index e26e0d0a..df0d7c25 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt @@ -6,8 +6,8 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.toArgb import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -49,7 +49,7 @@ class MainActivity : Activity2() { } setContent { - val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) + val themeState by generalSettings.themeState.collectAsStateWithLifecycle(initialValue = generalSettings.currentThemeState) val backStack = rememberNavBackStack(startDestination) navCtrl.setup(backStack) diff --git a/app/src/main/java/eu/darken/capod/main/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/onboarding/OnboardingScreen.kt index c4f81603..f5dc51af 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/onboarding/OnboardingScreen.kt @@ -26,7 +26,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 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.navigation.NavigationEventHandler diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt index b98d31a7..1c5efdd0 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt @@ -43,7 +43,7 @@ import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper 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.navigation.NavigationEventHandler import eu.darken.capod.common.permissions.Permission @@ -129,11 +129,11 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) { } } - val stateHolder = waitForState(vm.state) - val state = stateHolder.value ?: return + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) + val currentState = state ?: return OverviewScreen( - state = state, + state = currentState, onRequestPermission = { vm.requestPermission(it) }, onManageDevices = { vm.goToDeviceManager() }, onSettings = { vm.goToSettings() }, diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt index d226424d..43d29213 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt @@ -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.flow.SingleEventFlow 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.navigation.Nav import eu.darken.capod.common.permissions.Permission @@ -74,7 +74,7 @@ class OverviewViewModel @Inject constructor( monitorControl.startMonitor() } } - .shareLatest(scope = vmScope) + .asLiveState() private val updateTicker = channelFlow { while (isActive) { @@ -113,7 +113,7 @@ class OverviewViewModel @Inject constructor( upgradeInfo = upgradeInfo, showUnmatchedDevices = showUnmatched, ) - }.shareLatest(scope = vmScope) + }.asLiveState() data class State( val now: Instant, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt index 78cb33ed..db798cee 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt @@ -29,7 +29,7 @@ import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.BuildConfigWrap 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.navigation.Nav import eu.darken.capod.common.navigation.NavigationEventHandler @@ -41,7 +41,7 @@ fun SettingsScreenHost(vm: SettingsViewModel = hiltViewModel()) { ErrorEventHandler(vm) NavigationEventHandler(vm) - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) state?.let { SettingsScreen( state = it, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt index 339ec959..71ef5d1d 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt @@ -4,7 +4,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.WebpageTool import eu.darken.capod.common.coroutine.DispatcherProvider 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.upgrade.UpgradeRepo import kotlinx.coroutines.flow.map @@ -23,7 +23,7 @@ class SettingsViewModel @Inject constructor( val state = upgradeRepo.upgradeInfo .map { State(sponsorUrl = upgradeRepo.getSponsorUrl()) } - .shareLatest(scope = vmScope) + .asLiveState() fun openUrl(url: String) { webpageTool.open(url) diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt index 29886c7b..99891f55 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt @@ -51,7 +51,7 @@ import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper 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.navigation.NavigationEventHandler import eu.darken.capod.common.settings.SettingsBaseItem @@ -74,7 +74,7 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) { vm.launchUpgradeFlow.collect { action -> activity?.let { action(it) } } } - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) state?.let { GeneralSettingsScreen( state = it, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt index b2eb4ba3..ae76cacb 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt @@ -6,7 +6,6 @@ import eu.darken.capod.common.bluetooth.ScannerMode import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.logging.logTag 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.theming.ThemeColor import eu.darken.capod.common.theming.ThemeMode @@ -41,7 +40,7 @@ class GeneralSettingsViewModel @Inject constructor( 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>() @@ -77,7 +76,7 @@ class GeneralSettingsViewModel @Inject constructor( useIndirectScanResultCallback = compat[2] as Boolean, themeState = themeState, ) - }.shareLatest(scope = vmScope) + }.asLiveState() fun setMonitorMode(mode: MonitorMode) { generalSettings.monitorMode.value = mode diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsScreen.kt index 4ed8019f..1491ced6 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsScreen.kt @@ -23,7 +23,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 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.navigation.NavigationEventHandler import eu.darken.capod.common.settings.SettingsBaseItem @@ -33,7 +33,7 @@ fun DebugSettingsScreenHost(vm: DebugSettingsViewModel = hiltViewModel()) { ErrorEventHandler(vm) NavigationEventHandler(vm) - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) state?.let { DebugSettingsScreen( state = it, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsViewModel.kt index 3351601e..d0031bb9 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/debug/DebugSettingsViewModel.kt @@ -4,7 +4,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.DebugSettings import eu.darken.capod.common.debug.logging.logTag -import eu.darken.capod.common.flow.shareLatest + import eu.darken.capod.common.uix.ViewModel4 import kotlinx.coroutines.flow.combine import javax.inject.Inject @@ -31,7 +31,7 @@ class DebugSettingsViewModel @Inject constructor( showFakeData = fakeData, showUnfiltered = unfiltered, ) - }.shareLatest(scope = vmScope) + }.asLiveState() fun setDebugModeEnabled(enabled: Boolean) { debugSettings.isDebugModeEnabled.value = enabled diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportScreen.kt index 48670c1c..3885b539 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/support/SupportScreen.kt @@ -33,7 +33,7 @@ import eu.darken.capod.common.PrivacyPolicy import eu.darken.capod.common.WebpageTool import eu.darken.capod.common.compose.Preview2 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.error.ErrorEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler @@ -50,7 +50,7 @@ fun SupportScreenHost(vm: SupportViewModel = hiltViewModel()) { val context = LocalContext.current - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) var showShortRecordingWarning by remember { mutableStateOf(false) } diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/support/contactform/ContactFormScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/support/contactform/ContactFormScreen.kt index 48529b4d..5ca29a3c 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/support/contactform/ContactFormScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/support/contactform/ContactFormScreen.kt @@ -57,7 +57,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import eu.darken.capod.R 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.error.ErrorEventHandler 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 { ContactFormScreen( state = it, diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt index 224cb908..878a50b5 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt @@ -9,8 +9,8 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.toArgb import androidx.core.view.WindowCompat @@ -18,7 +18,6 @@ import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint 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.logTag import eu.darken.capod.common.theming.CapodTheme @@ -61,7 +60,7 @@ class WidgetConfigurationActivity : Activity2() { } setContent { - val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) + val themeState by generalSettings.themeState.collectAsStateWithLifecycle(initialValue = generalSettings.currentThemeState) CapodTheme(state = themeState) { val backgroundColor = MaterialTheme.colorScheme.background val useDarkIcons = backgroundColor.luminance() > 0.5f @@ -72,7 +71,7 @@ class WidgetConfigurationActivity : Activity2() { insetsController.isAppearanceLightNavigationBars = useDarkIcons } - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) state?.let { currentState -> WidgetConfigurationScreen( state = currentState, diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt index ebc4e6bc..b823ec47 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationViewModel.kt @@ -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.logTag 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.upgrade.UpgradeRepo import eu.darken.capod.profiles.core.DeviceProfile @@ -70,7 +69,7 @@ class WidgetConfigurationViewModel @Inject constructor( activePreset = activePreset, isCustomMode = activePreset == null, ) - }.shareLatest(scope = vmScope) + }.asLiveState() data class State( val profiles: List = emptyList(), diff --git a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt index b6c8252f..1079439c 100644 --- a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt +++ b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt @@ -39,7 +39,7 @@ import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper 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.navigation.NavigationEventHandler import eu.darken.capod.pods.core.PodDevice @@ -50,7 +50,7 @@ fun DeviceManagerScreenHost(vm: DeviceManagerViewModel = hiltViewModel()) { ErrorEventHandler(vm) NavigationEventHandler(vm) - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) state?.let { DeviceManagerScreen( state = it, diff --git a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt index ab4b9cb7..51fba592 100644 --- a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt +++ b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt @@ -4,7 +4,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.logging.log 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.uix.ViewModel4 import eu.darken.capod.profiles.core.DeviceProfile @@ -20,7 +20,7 @@ class DeviceManagerViewModel @Inject constructor( val state = deviceProfilesRepo.profiles.map { profiles -> State(profiles = profiles) - }.shareLatest(scope = vmScope) + }.asLiveState() data class State( val profiles: List, diff --git a/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationScreen.kt b/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationScreen.kt index aa612d96..e88a3fc9 100644 --- a/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationScreen.kt +++ b/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationScreen.kt @@ -61,7 +61,7 @@ import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BluetoothDevice2 import eu.darken.capod.common.compose.Preview2 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.navigation.NavigationEventHandler import eu.darken.capod.common.toHex @@ -77,7 +77,7 @@ fun DeviceProfileCreationScreenHost( ErrorEventHandler(vm) NavigationEventHandler(vm) - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) var showUnsavedChangesDialog by rememberSaveable { mutableStateOf(false) } var showDeleteDialog by rememberSaveable { mutableStateOf(false) } diff --git a/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt b/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt index 71de003a..9d7042c3 100644 --- a/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt +++ b/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt @@ -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.logTag 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.toHex import eu.darken.capod.common.uix.ViewModel4 @@ -118,7 +117,7 @@ class DeviceProfileCreationViewModel @Inject constructor( minimumSignalQuality = editorState.minimumSignalQuality, canSave = formValid && hasChanges, ) - }.shareLatest(scope = vmScope) + }.asLiveState() data class State( val isEditMode: Boolean, diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt index a8797a32..66ff6b8f 100644 --- a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt @@ -43,7 +43,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 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.navigation.NavigationEventHandler import eu.darken.capod.common.settings.SettingsBaseItem @@ -55,7 +55,7 @@ fun ReactionSettingsScreenHost(vm: ReactionSettingsViewModel = hiltViewModel()) ErrorEventHandler(vm) NavigationEventHandler(vm) - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) val activity = LocalContext.current as Activity state?.let { diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt index a305f158..453056c3 100644 --- a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt @@ -4,7 +4,7 @@ import android.app.Activity import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.coroutine.DispatcherProvider 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.upgrade.UpgradeRepo import eu.darken.capod.main.core.GeneralSettings @@ -35,7 +35,7 @@ class ReactionSettingsViewModel @Inject constructor( 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( isPro, @@ -57,7 +57,7 @@ class ReactionSettingsViewModel @Inject constructor( showPopUpOnCaseOpen = values[6] as Boolean, showPopUpOnConnection = values[7] as Boolean, ) - }.shareLatest(scope = vmScope) + }.asLiveState() fun setOnePodMode(enabled: Boolean) { reactionSettings.onePodMode.value = enabled diff --git a/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterScreen.kt b/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterScreen.kt index e58b1e20..614687d9 100644 --- a/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterScreen.kt +++ b/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterScreen.kt @@ -32,7 +32,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 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.navigation.NavigationEventHandler @@ -41,7 +41,7 @@ fun TroubleShooterScreenHost(vm: TroubleShooterViewModel = hiltViewModel()) { ErrorEventHandler(vm) NavigationEventHandler(vm) - val state by waitForState(vm.state) + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) state?.let { TroubleShooterScreen( state = it, diff --git a/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterViewModel.kt b/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterViewModel.kt index 3427ab0c..d9e14ef4 100644 --- a/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterViewModel.kt +++ b/app/src/main/java/eu/darken/capod/troubleshooter/ui/TroubleShooterViewModel.kt @@ -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.log 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.main.core.GeneralSettings import eu.darken.capod.monitor.core.PodMonitor @@ -45,7 +45,7 @@ class TroubleShooterViewModel @Inject constructor( data class State(val bleState: BleState) - val state = _bleState.map { State(it) }.shareLatest(scope = vmScope) + val state = _bleState.map { State(it) }.asLiveState() init { _bleState diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 4e7cce2d..7b1ba56b 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -112,6 +112,7 @@ fun DependencyHandlerScope.addCompose() { implementation("androidx.activity:activity-compose:1.12.1") 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") }