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
|
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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue