Skip to content

Commit 8983d6d

Browse files
committed
modalsheet: fix handling custom BackHandler in modalsheet
1 parent 47a91d4 commit 8983d6d

File tree

2 files changed

+32
-176
lines changed
  • demo/src/main/kotlin/dev/hrach/navigation/demo/screens
  • modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet

2 files changed

+32
-176
lines changed

demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
package dev.hrach.navigation.demo.screens
22

33
import android.annotation.SuppressLint
4+
import androidx.activity.compose.BackHandler
45
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.Spacer
58
import androidx.compose.foundation.layout.WindowInsets
69
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.height
711
import androidx.compose.foundation.layout.systemBars
812
import androidx.compose.foundation.layout.windowInsetsPadding
913
import androidx.compose.material3.MaterialTheme
1014
import androidx.compose.material3.OutlinedButton
1115
import androidx.compose.material3.Surface
16+
import androidx.compose.material3.Switch
1217
import androidx.compose.material3.Text
1318
import androidx.compose.runtime.Composable
1419
import androidx.compose.runtime.getValue
1520
import androidx.compose.runtime.mutableIntStateOf
21+
import androidx.compose.runtime.mutableStateOf
1622
import androidx.compose.runtime.remember
1723
import androidx.compose.runtime.saveable.rememberSaveable
1824
import androidx.compose.runtime.setValue
25+
import androidx.compose.ui.Alignment
1926
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.unit.dp
2028
import androidx.navigation.NavController
2129
import dev.hrach.navigation.demo.Destinations
2230
import dev.hrach.navigation.results.NavigationResultEffect
@@ -42,6 +50,11 @@ private fun Modal1(
4250
navigate: (Any) -> Unit,
4351
bottomSheetResult: Int,
4452
) {
53+
var disableBackHandling by rememberSaveable { mutableStateOf(false) }
54+
BackHandler(disableBackHandling) {
55+
// no-op
56+
}
57+
4558
Surface(
4659
color = MaterialTheme.colorScheme.inverseSurface,
4760
) {
@@ -58,6 +71,13 @@ private fun Modal1(
5871
Text("BottomSheet")
5972
}
6073
Text("BottomSheetResult: $bottomSheetResult")
74+
75+
Spacer(Modifier.height(32.dp))
76+
77+
Row(verticalAlignment = Alignment.CenterVertically) {
78+
Text("Disable back handling")
79+
Switch(disableBackHandling, onCheckedChange = { disableBackHandling = it })
80+
}
6181
}
6282
}
6383
}

modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetDialog.kt

Lines changed: 12 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,10 @@ import android.view.View
77
import android.view.ViewOutlineProvider
88
import android.view.Window
99
import android.view.WindowManager
10-
import android.window.BackEvent
11-
import android.window.OnBackAnimationCallback
12-
import android.window.OnBackInvokedCallback
13-
import android.window.OnBackInvokedDispatcher
1410
import androidx.activity.BackEventCompat
1511
import androidx.activity.ComponentDialog
16-
import androidx.activity.addCallback
12+
import androidx.activity.compose.PredictiveBackHandler
1713
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
18-
import androidx.annotation.DoNotInline
19-
import androidx.annotation.RequiresApi
2014
import androidx.appcompat.view.ContextThemeWrapper
2115
import androidx.compose.foundation.isSystemInDarkTheme
2216
import androidx.compose.foundation.layout.Box
@@ -29,7 +23,6 @@ import androidx.compose.runtime.getValue
2923
import androidx.compose.runtime.mutableStateOf
3024
import androidx.compose.runtime.remember
3125
import androidx.compose.runtime.rememberCompositionContext
32-
import androidx.compose.runtime.rememberCoroutineScope
3326
import androidx.compose.runtime.rememberUpdatedState
3427
import androidx.compose.runtime.saveable.rememberSaveable
3528
import androidx.compose.runtime.setValue
@@ -54,16 +47,7 @@ import androidx.lifecycle.setViewTreeViewModelStoreOwner
5447
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
5548
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
5649
import java.util.UUID
57-
import java.util.concurrent.CancellationException
58-
import kotlinx.coroutines.CoroutineScope
59-
import kotlinx.coroutines.channels.BufferOverflow.SUSPEND
60-
import kotlinx.coroutines.channels.Channel
61-
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
6250
import kotlinx.coroutines.flow.Flow
63-
import kotlinx.coroutines.flow.consumeAsFlow
64-
import kotlinx.coroutines.flow.flowOf
65-
import kotlinx.coroutines.flow.onCompletion
66-
import kotlinx.coroutines.launch
6751

6852
@Composable
6953
internal fun ModalSheetDialog(
@@ -79,18 +63,16 @@ internal fun ModalSheetDialog(
7963
val dialogId = rememberSaveable { UUID.randomUUID() }
8064
val darkThemeEnabled = isSystemInDarkTheme()
8165
val currentOnPredictiveBack = rememberUpdatedState(onPredictiveBack)
82-
val scope = rememberCoroutineScope()
8366

8467
val dialog = remember(view, density) {
8568
ModalSheetDialogWrapper(
86-
currentOnPredictiveBack,
87-
view,
88-
scope,
89-
securePolicy,
90-
layoutDirection,
91-
density,
92-
dialogId,
93-
darkThemeEnabled,
69+
onPredictiveBack = currentOnPredictiveBack,
70+
composeView = view,
71+
securePolicy = securePolicy,
72+
layoutDirection = layoutDirection,
73+
density = density,
74+
dialogId = dialogId,
75+
darkThemeEnabled = darkThemeEnabled,
9476
).apply {
9577
setContent(composition) {
9678
Box(
@@ -124,10 +106,8 @@ private class ModalSheetDialogLayout(
124106
context: Context,
125107
override val window: Window,
126108
private val onPredictiveBack: State<suspend (Flow<BackEventCompat>) -> Unit>,
127-
private val scope: CoroutineScope,
128109
) : AbstractComposeView(context), DialogWindowProvider {
129110
private var content: @Composable () -> Unit by mutableStateOf({})
130-
private var backCallback: Any? = null
131111
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
132112
private set
133113

@@ -140,111 +120,9 @@ private class ModalSheetDialogLayout(
140120

141121
@Composable
142122
override fun Content() {
123+
PredictiveBackHandler(onBack = onPredictiveBack.value)
143124
content()
144125
}
145-
146-
override fun onAttachedToWindow() {
147-
super.onAttachedToWindow()
148-
maybeRegisterBackCallback()
149-
}
150-
151-
override fun onDetachedFromWindow() {
152-
super.onDetachedFromWindow()
153-
maybeUnregisterBackCallback()
154-
}
155-
156-
private fun maybeRegisterBackCallback() {
157-
if (Build.VERSION.SDK_INT < 33) return
158-
if (backCallback == null) {
159-
backCallback = when {
160-
Build.VERSION.SDK_INT >= 34 -> Api34Impl.createBackCallback(onPredictiveBack, scope)
161-
else -> Api33Impl.createBackCallback(onPredictiveBack, scope)
162-
}
163-
}
164-
Api33Impl.maybeRegisterBackCallback(this, backCallback)
165-
}
166-
167-
private fun maybeUnregisterBackCallback() {
168-
if (Build.VERSION.SDK_INT >= 33) {
169-
Api33Impl.maybeUnregisterBackCallback(this, backCallback)
170-
}
171-
backCallback = null
172-
}
173-
174-
@RequiresApi(34)
175-
private object Api34Impl {
176-
@JvmStatic
177-
@DoNotInline
178-
fun createBackCallback(
179-
currentOnBack: State<suspend (Flow<BackEventCompat>) -> Unit>,
180-
scope: CoroutineScope,
181-
) = object : OnBackAnimationCallback {
182-
var onBackInstance: OnBackInstance? = null
183-
184-
override fun onBackStarted(backEvent: BackEvent) {
185-
onBackInstance?.cancel()
186-
onBackInstance = OnBackInstance(scope, true, currentOnBack.value)
187-
}
188-
189-
override fun onBackProgressed(backEvent: BackEvent) {
190-
onBackInstance?.send(BackEventCompat(backEvent))
191-
}
192-
193-
override fun onBackInvoked() {
194-
onBackInstance?.apply {
195-
if (!isPredictiveBack) {
196-
cancel()
197-
onBackInstance = null
198-
}
199-
}
200-
if (onBackInstance == null) {
201-
onBackInstance = OnBackInstance(scope, false, currentOnBack.value)
202-
}
203-
onBackInstance?.close()
204-
onBackInstance?.isPredictiveBack = false
205-
}
206-
207-
override fun onBackCancelled() {
208-
onBackInstance?.cancel()
209-
onBackInstance = null
210-
onBackInstance?.isPredictiveBack = false
211-
}
212-
}
213-
}
214-
215-
@RequiresApi(33)
216-
private object Api33Impl {
217-
@JvmStatic
218-
@DoNotInline
219-
fun createBackCallback(
220-
currentOnBack: State<suspend (Flow<BackEventCompat>) -> Unit>,
221-
scope: CoroutineScope,
222-
) {
223-
OnBackInvokedCallback {
224-
scope.launch {
225-
currentOnBack.value.invoke(flowOf())
226-
}
227-
}
228-
}
229-
230-
@JvmStatic
231-
@DoNotInline
232-
fun maybeRegisterBackCallback(view: View, backCallback: Any?) {
233-
if (backCallback !is OnBackInvokedCallback) return
234-
val dispatcher = view.findOnBackInvokedDispatcher() ?: return
235-
dispatcher.registerOnBackInvokedCallback(
236-
OnBackInvokedDispatcher.PRIORITY_OVERLAY,
237-
backCallback,
238-
)
239-
}
240-
241-
@JvmStatic
242-
@DoNotInline
243-
fun maybeUnregisterBackCallback(view: View, backCallback: Any?) {
244-
if (backCallback !is OnBackInvokedCallback) return
245-
view.findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(backCallback)
246-
}
247-
}
248126
}
249127

250128
// Fork of androidx.compose.ui.window.DialogWrapper.
@@ -253,7 +131,6 @@ private class ModalSheetDialogLayout(
253131
internal class ModalSheetDialogWrapper(
254132
onPredictiveBack: State<suspend (Flow<BackEventCompat>) -> Unit>,
255133
private val composeView: View,
256-
scope: CoroutineScope,
257134
securePolicy: SecureFlagPolicy,
258135
layoutDirection: LayoutDirection,
259136
density: Density,
@@ -274,10 +151,9 @@ internal class ModalSheetDialogWrapper(
274151
window.setBackgroundDrawableResource(android.R.color.transparent)
275152
WindowCompat.setDecorFitsSystemWindows(window, false)
276153
dialogLayout = ModalSheetDialogLayout(
277-
context,
278-
window,
279-
onPredictiveBack,
280-
scope,
154+
context = context,
155+
window = window,
156+
onPredictiveBack = onPredictiveBack,
281157
).apply {
282158
// Set unique id for AbstractComposeView. This allows state restoration for the state
283159
// defined inside the Dialog via rememberSaveable()
@@ -310,17 +186,6 @@ internal class ModalSheetDialogWrapper(
310186
dialogLayout.setViewTreeOnBackPressedDispatcherOwner(this)
311187
// Initial setup
312188
updateParameters(securePolicy, layoutDirection, darkThemeEnabled)
313-
314-
// Due to how the onDismissRequest callback works
315-
// (it enforces a just-in-time decision on whether to update the state to hide the dialog)
316-
// we need to unconditionally add a callback here that is always enabled,
317-
// meaning we'll never get a system UI controlled predictive back animation
318-
// for these dialogs
319-
onBackPressedDispatcher.addCallback(this) {
320-
scope.launch {
321-
onPredictiveBack.value.invoke(flowOf())
322-
}
323-
}
324189
}
325190

326191
private fun setLayoutDirection(layoutDirection: LayoutDirection) {
@@ -383,35 +248,6 @@ internal class ModalSheetDialogWrapper(
383248
}
384249
}
385250

386-
private class OnBackInstance(
387-
scope: CoroutineScope,
388-
var isPredictiveBack: Boolean,
389-
onBack: suspend (progress: Flow<BackEventCompat>) -> Unit,
390-
) {
391-
val channel = Channel<BackEventCompat>(capacity = BUFFERED, onBufferOverflow = SUSPEND)
392-
val job = scope.launch {
393-
var completed = false
394-
onBack(
395-
channel.consumeAsFlow().onCompletion {
396-
completed = true
397-
},
398-
)
399-
check(completed) {
400-
"You must collect the progress flow"
401-
}
402-
}
403-
404-
fun send(backEvent: BackEventCompat) = channel.trySend(backEvent)
405-
406-
// idempotent if invoked more than once
407-
fun close() = channel.close()
408-
409-
fun cancel() {
410-
channel.cancel(CancellationException("onBack cancelled"))
411-
job.cancel()
412-
}
413-
}
414-
415251
internal fun View.isFlagSecureEnabled(): Boolean {
416252
val windowParams = rootView.layoutParams as? WindowManager.LayoutParams
417253
if (windowParams != null) {

0 commit comments

Comments
 (0)