fix: Intercept the back press only when the screen is visible
This commit is contained in:
parent
a239e3e8f3
commit
8d1f92bbbe
|
@ -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(
|
|||
}
|
||||
}
|
||||
|
||||
val job = scope.launch {
|
||||
interceptorFlow
|
||||
.map { c ->
|
||||
val newCallback = c.takeIf { subScope.isActive }
|
||||
val newCallback = c.takeIf { this.isActive }
|
||||
setCallback(newCallback)
|
||||
}
|
||||
.launchIn(subScope)
|
||||
return {
|
||||
subScope.cancel()
|
||||
.launchIn(this)
|
||||
|
||||
try {
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
// Unregister the existing interceptor,
|
||||
// if there's any.
|
||||
unregister?.invoke()
|
||||
unregister = null
|
||||
}
|
||||
}
|
||||
return {
|
||||
job.cancel()
|
||||
// Unregister the existing interceptor,
|
||||
// if there's any.
|
||||
unregister?.invoke()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
): () -> 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,
|
||||
|
|
Loading…
Reference in New Issue