diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/backpress/BackPressInterceptorUtil.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/backpress/BackPressInterceptorUtil.kt index 2b5a02b..e460cdc 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/backpress/BackPressInterceptorUtil.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/backpress/BackPressInterceptorUtil.kt @@ -1,20 +1,17 @@ package com.artemchep.keyguard.feature.navigation.backpress import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive -import kotlinx.coroutines.plus +import kotlinx.coroutines.launch fun BackPressInterceptorHost.interceptBackPress( scope: CoroutineScope, interceptorFlow: Flow<(() -> Unit)?>, ): () -> Unit { - val subScope = scope + Job() - var callback: (() -> Unit)? = null // A registration to remove the back press interceptor // from the navigation entry. When it's not null it means @@ -44,14 +41,25 @@ fun BackPressInterceptorHost.interceptBackPress( } } - interceptorFlow - .map { c -> - val newCallback = c.takeIf { subScope.isActive } - setCallback(newCallback) + val job = scope.launch { + interceptorFlow + .map { c -> + val newCallback = c.takeIf { this.isActive } + setCallback(newCallback) + } + .launchIn(this) + + try { + awaitCancellation() + } finally { + // Unregister the existing interceptor, + // if there's any. + unregister?.invoke() + unregister = null } - .launchIn(subScope) + } return { - subScope.cancel() + job.cancel() // Unregister the existing interceptor, // if there's any. unregister?.invoke() diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/FlowHolderViewModel.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/FlowHolderViewModel.kt index 4b3d24a..ade9209 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/FlowHolderViewModel.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/FlowHolderViewModel.kt @@ -46,7 +46,7 @@ class FlowHolderViewModel( screenName: String, context: LeContext, colorSchemeState: State, - init: RememberStateFlowScope.() -> T, + init: RememberStateFlowScopeZygote.() -> T, ): T = synchronized(this) { store.getOrPut(key) { val vmCoroutineScopeJob = SupervisorJob() diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberScreenStateFlow.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberScreenStateFlow.kt index 1ed0a23..219fd2d 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberScreenStateFlow.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberScreenStateFlow.kt @@ -20,15 +20,15 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.withIndex -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.datetime.Clock import kotlinx.serialization.json.Json @@ -76,17 +76,7 @@ fun rememberScreenStateFlow( *rargs, ) { val now = Clock.System.now() - val flow: RememberStateFlowScope.() -> Flow = { - launch { -// Log.i(finalTag, "Initialized the state of '$finalKey'.") - try { - // Suspend forever. - suspendCancellableCoroutine { } - } finally { -// Log.i(finalTag, "Finished the state of '$finalKey'.") - } - } - + val flow: RememberStateFlowScopeZygote.() -> Flow = { val structureFlow = flow { val shouldDelay = getDebugScreenDelay().firstOrNull() == true if (shouldDelay) { @@ -101,12 +91,17 @@ fun rememberScreenStateFlow( // We don't want to recreate a state producer // each time we re-subscribe to it. .shareIn(this, SharingStarted.Lazily, 1) - structureFlow + val stateFlow = structureFlow .flattenConcat() .withIndex() .map { it.value } .flowOn(Dispatchers.Default) - .persistingStateIn(this, SharingStarted.WhileSubscribed(), initial) + merge( + stateFlow, + keepAliveFlow + .filter { false } as Flow, + ) + .persistingStateIn(screenScope, SharingStarted.WhileSubscribed(), initial) } val flow2 = viewModel.getOrPut( finalKey, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScope.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScope.kt index ab6fc73..385a81e 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScope.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScope.kt @@ -86,6 +86,10 @@ interface RememberStateFlowScope : RememberStateFlowScopeSub, CoroutineScope, Tr interceptorFlow: Flow<(() -> Unit)?>, ): () -> Unit + fun launchUi( + block: CoroutineScope.() -> Unit, + ): () -> Unit + // // Helpers // @@ -101,6 +105,10 @@ interface RememberStateFlowScope : RememberStateFlowScopeSub, CoroutineScope, Tr ) } +interface RememberStateFlowScopeZygote : RememberStateFlowScope { + val keepAliveFlow: Flow +} + fun RememberStateFlowScope.navigatePopSelf() { val intent = NavigationIntent.PopById(screenId, exclusive = false) navigate(intent) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScopeImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScopeImpl.kt index 42ee3f2..8389593 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScopeImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/navigation/state/RememberStateFlowScopeImpl.kt @@ -29,10 +29,14 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.plus import kotlinx.serialization.json.Json @@ -52,7 +56,7 @@ class RememberStateFlowScopeImpl( private val colorSchemeState: State, override val screenName: String, override val context: LeContext, -) : RememberStateFlowScope, CoroutineScope by scope { +) : RememberStateFlowScopeZygote, CoroutineScope by scope { private val registry = mutableMapOf>() override val colorScheme get() = colorSchemeState.value @@ -98,6 +102,20 @@ class RememberStateFlowScopeImpl( override val screenId: String get() = screen + /** + * A flow that is getting observed while the user interface is + * visible on a screen. Used to provide lifecycle events for the + * screen. + */ + private val keepAliveSharedFlow = MutableSharedFlow() + + private val isStartedFlow = keepAliveSharedFlow + .subscriptionCount + .map { it > 0 } + .distinctUntilChanged() + + override val keepAliveFlow get() = keepAliveSharedFlow + private fun getBundleKey(key: String) = "${this.key}:$key" override fun navigate( @@ -163,10 +181,35 @@ class RememberStateFlowScopeImpl( override fun interceptBackPress( interceptorFlow: Flow<(() -> Unit)?>, - ) = backPressInterceptorHost.interceptBackPress( - scope = scope, - interceptorFlow = interceptorFlow, - ) + ): () -> Unit { + // We want to launch back interceptor only when the + // screen is currently added to the composable. Otherwise + // it would intercept the back press event if visually invisible + // to a user. + return launchUi { + backPressInterceptorHost.interceptBackPress( + scope = this, + interceptorFlow = interceptorFlow, + ) + } + } + + override fun launchUi(block: CoroutineScope.() -> Unit): () -> Unit { + val job = isStartedFlow + .mapLatest { active -> + if (!active) { + return@mapLatest + } + + coroutineScope { + block() + } + } + .launchIn(scope) + return { + job.cancel() + } + } override suspend fun loadDiskHandle( key: String,