feature: Cancel the selection on back press #26
This commit is contained in:
parent
3de0e3734e
commit
b4859a19fb
@ -1,17 +1,25 @@
|
||||
package com.artemchep.keyguard.feature.navigation
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import com.artemchep.keyguard.feature.navigation.backpress.BackPressInterceptorHost
|
||||
import com.artemchep.keyguard.feature.navigation.backpress.BackPressInterceptorRegistration
|
||||
import com.artemchep.keyguard.feature.navigation.state.FlowHolderViewModel
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
|
||||
interface NavigationEntry {
|
||||
interface NavigationEntry : BackPressInterceptorHost {
|
||||
companion object {
|
||||
fun new() {
|
||||
}
|
||||
@ -24,6 +32,8 @@ interface NavigationEntry {
|
||||
val scope: CoroutineScope
|
||||
val vm: FlowHolderViewModel
|
||||
|
||||
val activeBackPressInterceptorsStateFlow: StateFlow<ImmutableMap<String, BackPressInterceptorRegistration>>
|
||||
|
||||
fun destroy()
|
||||
}
|
||||
|
||||
@ -43,7 +53,14 @@ data class NavigationEntryImpl(
|
||||
|
||||
override val scope = parent + job
|
||||
|
||||
override val vm: FlowHolderViewModel = FlowHolderViewModel(source, scope)
|
||||
override val vm: FlowHolderViewModel = FlowHolderViewModel(this)
|
||||
|
||||
private val activeBackPressInterceptorsStateSink = MutableStateFlow(
|
||||
persistentMapOf<String, BackPressInterceptorRegistration>(),
|
||||
)
|
||||
|
||||
override val activeBackPressInterceptorsStateFlow: StateFlow<ImmutableMap<String, BackPressInterceptorRegistration>>
|
||||
get() = activeBackPressInterceptorsStateSink
|
||||
|
||||
init {
|
||||
require('/' !in id)
|
||||
@ -60,6 +77,24 @@ data class NavigationEntryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun interceptBackPress(
|
||||
block: () -> Unit,
|
||||
): () -> Unit {
|
||||
val id = UUID.randomUUID().toString()
|
||||
val entry = BackPressInterceptorRegistration(
|
||||
id = id,
|
||||
block = block,
|
||||
)
|
||||
activeBackPressInterceptorsStateSink.update { state ->
|
||||
state.put(id, entry)
|
||||
}
|
||||
return {
|
||||
activeBackPressInterceptorsStateSink.update { state ->
|
||||
state.remove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
vm.destroy()
|
||||
job.cancel()
|
||||
|
@ -11,6 +11,8 @@ import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.UUID
|
||||
|
||||
@ -36,12 +38,40 @@ fun NavigationRouter(
|
||||
}
|
||||
val canPop = remember(navStack) {
|
||||
snapshotFlow { navStack.value }
|
||||
.map { it.size > 1 }
|
||||
.flatMapLatest { stack ->
|
||||
val navEntry = stack.lastOrNull()
|
||||
if (navEntry != null) {
|
||||
navEntry
|
||||
.activeBackPressInterceptorsStateFlow
|
||||
.map { interceptors ->
|
||||
interceptors.isNotEmpty() || stack.size > 1
|
||||
}
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigationController(
|
||||
canPop = canPop,
|
||||
handle = { intent ->
|
||||
val backStack = navStack.value
|
||||
|
||||
// If the navigation intent is a simple pop, then give it to
|
||||
// the back press interceptors first and only then adjust the
|
||||
// navigation stack.
|
||||
if (intent is NavigationIntent.Pop) {
|
||||
val backPressInterceptorRegistration = backStack
|
||||
.asReversed()
|
||||
.firstNotNullOfOrNull { navEntry ->
|
||||
val backPressInterceptors = navEntry.activeBackPressInterceptorsStateFlow.value
|
||||
backPressInterceptors.values.firstOrNull()
|
||||
}
|
||||
if (backPressInterceptorRegistration != null) {
|
||||
backPressInterceptorRegistration.block()
|
||||
return@NavigationController null
|
||||
}
|
||||
}
|
||||
|
||||
val newBackStack = backStack
|
||||
.exec(
|
||||
intent = intent,
|
||||
|
@ -0,0 +1,12 @@
|
||||
package com.artemchep.keyguard.feature.navigation.backpress
|
||||
|
||||
interface BackPressInterceptorHost {
|
||||
/**
|
||||
* Invoke to add the back press interceptor. The [block] function will be
|
||||
* called instead of altering the navigation stack. To remove the interceptor
|
||||
* call the returned lambda.
|
||||
*/
|
||||
fun interceptBackPress(
|
||||
block: () -> Unit,
|
||||
): () -> Unit
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.artemchep.keyguard.feature.navigation.backpress
|
||||
|
||||
data class BackPressInterceptorRegistration(
|
||||
val id: String,
|
||||
val block: () -> Unit,
|
||||
)
|
@ -0,0 +1,57 @@
|
||||
package com.artemchep.keyguard.feature.navigation.backpress
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
fun BackPressInterceptorHost.interceptBackPress(
|
||||
scope: CoroutineScope,
|
||||
interceptorFlow: Flow<(() -> Unit)?>,
|
||||
): () -> Unit {
|
||||
lateinit var job: Job
|
||||
|
||||
var callback: (() -> Unit)? = null
|
||||
// A registration to remove the back press interceptor
|
||||
// from the navigation entry. When it's not null it means
|
||||
// that the interceptor is active.
|
||||
var unregister: (() -> Unit)? = null
|
||||
|
||||
fun setCallback(
|
||||
cb: (() -> Unit)?,
|
||||
) {
|
||||
callback = cb
|
||||
if (callback != null) {
|
||||
if (unregister != null) {
|
||||
// Do nothing
|
||||
} else {
|
||||
// Register a new back press handler,
|
||||
// use the callback holder, so we do
|
||||
// not have to re-register in the future.
|
||||
unregister = interceptBackPress {
|
||||
val cb = callback
|
||||
?: return@interceptBackPress
|
||||
cb.invoke()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unregister?.invoke()
|
||||
unregister = null
|
||||
}
|
||||
}
|
||||
|
||||
job = interceptorFlow
|
||||
.map { c ->
|
||||
val newCallback = c.takeIf { !job.isCancelled }
|
||||
setCallback(newCallback)
|
||||
}
|
||||
.launchIn(scope)
|
||||
return {
|
||||
job.cancel()
|
||||
// Unregister the existing interceptor,
|
||||
// if there's any.
|
||||
unregister?.invoke()
|
||||
unregister = null
|
||||
}
|
||||
}
|
@ -8,18 +8,17 @@ import com.artemchep.keyguard.common.usecase.PutScreenState
|
||||
import com.artemchep.keyguard.common.usecase.ShowMessage
|
||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationController
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationEntry
|
||||
import com.artemchep.keyguard.platform.LeBundle
|
||||
import com.artemchep.keyguard.platform.LeContext
|
||||
import com.artemchep.keyguard.platform.leBundleOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class FlowHolderViewModel(
|
||||
private val source: String,
|
||||
private val scope: CoroutineScope,
|
||||
private val navigationEntry: NavigationEntry,
|
||||
) {
|
||||
var bundle: LeBundle = leBundleOf()
|
||||
|
||||
@ -31,6 +30,8 @@ class FlowHolderViewModel(
|
||||
val value: Any?,
|
||||
)
|
||||
|
||||
private val scope get() = navigationEntry.scope
|
||||
|
||||
fun <T> getOrPut(
|
||||
key: String,
|
||||
c: NavigationController,
|
||||
@ -51,6 +52,7 @@ class FlowHolderViewModel(
|
||||
key = key,
|
||||
scope = scope + job + Dispatchers.Default,
|
||||
navigationController = c,
|
||||
backPressInterceptorHost = navigationEntry,
|
||||
showMessage = showMessage,
|
||||
getScreenState = getScreenState,
|
||||
putScreenState = putScreenState,
|
||||
|
@ -79,14 +79,12 @@ interface RememberStateFlowScope : RememberStateFlowScopeSub, CoroutineScope, Tr
|
||||
)
|
||||
|
||||
/**
|
||||
* Register a listener to back press. Once registered, you must
|
||||
* call the callback with `true` if you can go back, `false` if that
|
||||
* should be ignored.
|
||||
* Register a back press interceptor. Once registered, the view model subscribes to
|
||||
* the flow and if the given lambda is not empty -> invokes it on back press event.
|
||||
*/
|
||||
fun interceptBackPress(
|
||||
isEnabledFlow: Flow<Boolean>,
|
||||
callback: ((Boolean) -> Unit) -> Unit,
|
||||
)
|
||||
interceptorFlow: Flow<(() -> Unit)?>,
|
||||
): () -> Unit
|
||||
|
||||
//
|
||||
// Helpers
|
||||
|
@ -16,6 +16,8 @@ import com.artemchep.keyguard.feature.loading.getErrorReadableMessage
|
||||
import com.artemchep.keyguard.feature.localization.textResource
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationController
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.backpress.BackPressInterceptorHost
|
||||
import com.artemchep.keyguard.feature.navigation.backpress.interceptBackPress
|
||||
import com.artemchep.keyguard.platform.LeBundle
|
||||
import com.artemchep.keyguard.platform.LeContext
|
||||
import com.artemchep.keyguard.platform.contains
|
||||
@ -43,6 +45,7 @@ class RememberStateFlowScopeImpl(
|
||||
private val putScreenState: PutScreenState,
|
||||
private val windowCoroutineScope: WindowCoroutineScope,
|
||||
private val navigationController: NavigationController,
|
||||
private val backPressInterceptorHost: BackPressInterceptorHost,
|
||||
private val json: Json,
|
||||
private val scope: CoroutineScope,
|
||||
private val screen: String,
|
||||
@ -157,10 +160,11 @@ class RememberStateFlowScopeImpl(
|
||||
}
|
||||
|
||||
override fun interceptBackPress(
|
||||
isEnabledFlow: Flow<Boolean>,
|
||||
callback: ((Boolean) -> Unit) -> Unit,
|
||||
) {
|
||||
}
|
||||
interceptorFlow: Flow<(() -> Unit)?>,
|
||||
) = backPressInterceptorHost.interceptBackPress(
|
||||
scope = scope,
|
||||
interceptorFlow = interceptorFlow,
|
||||
)
|
||||
|
||||
override suspend fun loadDiskHandle(
|
||||
key: String,
|
||||
|
@ -21,20 +21,24 @@ fun RememberStateFlowScope.selectionHandle(
|
||||
setOf<String>()
|
||||
}
|
||||
|
||||
// Intercept the back button while the
|
||||
// selection set is not empty.
|
||||
interceptBackPress(
|
||||
// Intercept the back button while the
|
||||
// selection set is not empty.
|
||||
isEnabledFlow = itemIdsSink
|
||||
.map { it.isNotEmpty() },
|
||||
) { callback ->
|
||||
val wereEmpty = itemIdsSink.value.isEmpty()
|
||||
// Reset the selection on back
|
||||
// button press.
|
||||
itemIdsSink.value = emptySet()
|
||||
|
||||
// Eat the callback if it weren't empty.
|
||||
callback(wereEmpty)
|
||||
}
|
||||
interceptorFlow = itemIdsSink
|
||||
.map { it.isNotEmpty() }
|
||||
.map { enabled ->
|
||||
if (enabled) {
|
||||
// lambda
|
||||
{
|
||||
// Reset the selection on back
|
||||
// button press.
|
||||
itemIdsSink.value = emptySet()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return object : SelectionHandle {
|
||||
override val idsFlow: StateFlow<Set<String>> get() = itemIdsSink
|
||||
|
Loading…
x
Reference in New Issue
Block a user