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 package com.artemchep.keyguard.feature.navigation.backpress
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.plus import kotlinx.coroutines.launch
fun BackPressInterceptorHost.interceptBackPress( fun BackPressInterceptorHost.interceptBackPress(
scope: CoroutineScope, scope: CoroutineScope,
interceptorFlow: Flow<(() -> Unit)?>, interceptorFlow: Flow<(() -> Unit)?>,
): () -> Unit { ): () -> Unit {
val subScope = scope + Job()
var callback: (() -> Unit)? = null var callback: (() -> Unit)? = null
// A registration to remove the back press interceptor // A registration to remove the back press interceptor
// from the navigation entry. When it's not null it means // from the navigation entry. When it's not null it means
@ -44,14 +41,25 @@ fun BackPressInterceptorHost.interceptBackPress(
} }
} }
val job = scope.launch {
interceptorFlow interceptorFlow
.map { c -> .map { c ->
val newCallback = c.takeIf { subScope.isActive } val newCallback = c.takeIf { this.isActive }
setCallback(newCallback) setCallback(newCallback)
} }
.launchIn(subScope) .launchIn(this)
return {
subScope.cancel() try {
awaitCancellation()
} finally {
// Unregister the existing interceptor,
// if there's any.
unregister?.invoke()
unregister = null
}
}
return {
job.cancel()
// Unregister the existing interceptor, // Unregister the existing interceptor,
// if there's any. // if there's any.
unregister?.invoke() unregister?.invoke()

View File

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

View File

@ -20,15 +20,15 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flattenConcat
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -76,17 +76,7 @@ fun <T> rememberScreenStateFlow(
*rargs, *rargs,
) { ) {
val now = Clock.System.now() val now = Clock.System.now()
val flow: RememberStateFlowScope.() -> Flow<T> = { val flow: RememberStateFlowScopeZygote.() -> 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 structureFlow = flow { val structureFlow = flow {
val shouldDelay = getDebugScreenDelay().firstOrNull() == true val shouldDelay = getDebugScreenDelay().firstOrNull() == true
if (shouldDelay) { if (shouldDelay) {
@ -101,12 +91,17 @@ fun <T> rememberScreenStateFlow(
// We don't want to recreate a state producer // We don't want to recreate a state producer
// each time we re-subscribe to it. // each time we re-subscribe to it.
.shareIn(this, SharingStarted.Lazily, 1) .shareIn(this, SharingStarted.Lazily, 1)
structureFlow val stateFlow = structureFlow
.flattenConcat() .flattenConcat()
.withIndex() .withIndex()
.map { it.value } .map { it.value }
.flowOn(Dispatchers.Default) .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( val flow2 = viewModel.getOrPut(
finalKey, finalKey,

View File

@ -86,6 +86,10 @@ interface RememberStateFlowScope : RememberStateFlowScopeSub, CoroutineScope, Tr
interceptorFlow: Flow<(() -> Unit)?>, interceptorFlow: Flow<(() -> Unit)?>,
): () -> Unit ): () -> Unit
fun launchUi(
block: CoroutineScope.() -> Unit,
): () -> Unit
// //
// Helpers // Helpers
// //
@ -101,6 +105,10 @@ interface RememberStateFlowScope : RememberStateFlowScopeSub, CoroutineScope, Tr
) )
} }
interface RememberStateFlowScopeZygote : RememberStateFlowScope {
val keepAliveFlow: Flow<Unit>
}
fun RememberStateFlowScope.navigatePopSelf() { fun RememberStateFlowScope.navigatePopSelf() {
val intent = NavigationIntent.PopById(screenId, exclusive = false) val intent = NavigationIntent.PopById(screenId, exclusive = false)
navigate(intent) navigate(intent)

View File

@ -29,10 +29,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -52,7 +56,7 @@ class RememberStateFlowScopeImpl(
private val colorSchemeState: State<ColorScheme>, private val colorSchemeState: State<ColorScheme>,
override val screenName: String, override val screenName: String,
override val context: LeContext, override val context: LeContext,
) : RememberStateFlowScope, CoroutineScope by scope { ) : RememberStateFlowScopeZygote, CoroutineScope by scope {
private val registry = mutableMapOf<String, Entry<Any?, Any?>>() private val registry = mutableMapOf<String, Entry<Any?, Any?>>()
override val colorScheme get() = colorSchemeState.value override val colorScheme get() = colorSchemeState.value
@ -98,6 +102,20 @@ class RememberStateFlowScopeImpl(
override val screenId: String override val screenId: String
get() = screen 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" private fun getBundleKey(key: String) = "${this.key}:$key"
override fun navigate( override fun navigate(
@ -163,10 +181,35 @@ class RememberStateFlowScopeImpl(
override fun interceptBackPress( override fun interceptBackPress(
interceptorFlow: Flow<(() -> Unit)?>, interceptorFlow: Flow<(() -> Unit)?>,
) = backPressInterceptorHost.interceptBackPress( ): () -> Unit {
scope = scope, // 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, 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( override suspend fun loadDiskHandle(
key: String, key: String,