fix: Intercept the back press only when the screen is visible

This commit is contained in:
Artem Chepurnoy 2024-02-27 19:51:44 +02:00
parent a239e3e8f3
commit 8d1f92bbbe
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
5 changed files with 86 additions and 32 deletions

View File

@ -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()

View File

@ -46,7 +46,7 @@ class FlowHolderViewModel(
screenName: String,
context: LeContext,
colorSchemeState: State<ColorScheme>,
init: RememberStateFlowScope.() -> T,
init: RememberStateFlowScopeZygote.() -> T,
): T = synchronized(this) {
store.getOrPut(key) {
val vmCoroutineScopeJob = SupervisorJob()

View File

@ -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 <T> rememberScreenStateFlow(
*rargs,
) {
val now = Clock.System.now()
val flow: RememberStateFlowScope.() -> Flow<T> = {
launch {
// Log.i(finalTag, "Initialized the state of '$finalKey'.")
try {
// Suspend forever.
suspendCancellableCoroutine<Unit> { }
} finally {
// Log.i(finalTag, "Finished the state of '$finalKey'.")
}
}
val flow: RememberStateFlowScopeZygote.() -> Flow<T> = {
val structureFlow = flow {
val shouldDelay = getDebugScreenDelay().firstOrNull() == true
if (shouldDelay) {
@ -101,12 +91,17 @@ fun <T> 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<T>,
)
.persistingStateIn(screenScope, SharingStarted.WhileSubscribed(), initial)
}
val flow2 = viewModel.getOrPut(
finalKey,

View File

@ -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<Unit>
}
fun RememberStateFlowScope.navigatePopSelf() {
val intent = NavigationIntent.PopById(screenId, exclusive = false)
navigate(intent)

View File

@ -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<ColorScheme>,
override val screenName: String,
override val context: LeContext,
) : RememberStateFlowScope, CoroutineScope by scope {
) : RememberStateFlowScopeZygote, CoroutineScope by scope {
private val registry = mutableMapOf<String, Entry<Any?, Any?>>()
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<Unit>()
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,