When implementing `Scaffold` with a navigation dra...
# compose
m
When implementing
Scaffold
with a navigation drawer, I noticed that back handling (i.e. when drawer is open, back button closes the drawer) is not working out of the box. Which approach do you use to handle this? Overriding onBackPressed; onBackPressedDispatcher; compose BackHandler; adding an entry to back stack; something else? Note: I’m still using legacy androidx navigation.
j
See composable BackHandler 🙂 I usually combined that with Drawers or bottomsheets. Example:
val sheetState = _rememberModalBottomSheetState_(skipPartiallyExpanded = true)
val coroutineScope = _rememberCoroutineScope_()
_BackHandler_(enabled = sheetState.isVisible) *{*
coroutineScope
._launch_ *{* sheetState.hide() *}*
.invokeOnCompletion *{*
if (!sheetState.isVisible) {
navigator.finish(onDismiss())
}
}
}
_ModalBottomSheet_(
modifier = Modifier._fillMaxWidth_(),
content = *{*
content(model) *{* result *->*
coroutineScope._launch_ *{*
try {
sheetState.hide()
} finally {
navigator.finish(result)
}
}
}
*}*,
sheetState = sheetState,
onDismissRequest = *{* navigator.finish(onDismiss()) *}*,
)
m
Thanks Joel. It’s not quite working for me. It works fine when I’m on the top-level screen, but when I’ve already navigated to another screen, the back handling of the navigation library takes priority. Any ideas? i.e. Navigate to screen / Open Drawer / then hit back (expected: close drawer, actual: navigate back to home screen) EDIT: I worked it out. The
BackHandler
needs to be used in the
Scaffold
content
slot, not the
drawerContent
slot
j
Ah okay you have nested BackHandlers then. Hmm I think each one adding a new BackHandler callback to the root Activity back handler owner thing. It begins with bottom and push upwards if I remember correct. I think the best is having BackHandler top level and make sure only using one at the time and avoid nested child ones as much as possible 🙂
👍 1
Thats what I like with the Circuit library I am using, with content overlay for dialogs, bottomsheets, drawers etc. Having it delegated at navigation top level 🙂
@Mark I think you could also add BackHandler outside of Scaffold as well 🙂 also drawerContent is "destroys" itself when not visible, it will not work all the time. Be careful with enabled = true|false based on if blocks as well. Easy to do it backwards for back navigation, like using combined with navigationIcon in top bar and drawer at the same time 😄
m
Interestingly, when I move it out of scaffold it doesn’t work (i.e. wrong priority). Interestingly, a small part of my UI uses Voyager for its own navigation and that seems to take precedence over everything else (including the BackHandler no matter where I put it), so it looks like I’ll have to try what you are suggesting above.
If you look at how compose
Dialog
works, it uses:
Copy code
onBackPressedDispatcher.addCallback(this) {
    if (properties.dismissOnBackPress) {
        onDismissRequest()
    }
}
although that of course is Android-only
j
Hmm ok maybe Voyager hooks up into same back handler owner mechanism as well and interact? Can always check in Activity level which dispatcher callbacks added and in which order 🙂
You can use
LocalOnBackPressedDispatcherOwner
as well btw 🙂
m
Yes that works, though I had to put it in the
drawerContent
slot like this:
Copy code
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
if (onBackPressedDispatcher != null && scaffoldState.drawerState.isOpen) {
    DisposableEffect(Unit) {
        val callback = object : OnBackPressedCallback(enabled = true) {
            override fun handleOnBackPressed() {
                scope.launch {
                    scaffoldState.drawerState.close()
                }
            }
        }
        onBackPressedDispatcher.addCallback(callback)
        onDispose {
            callback.remove()
        }
    }
}
Ah, so now it works with (in drawerContent) like this:
Copy code
if (scaffoldState.drawerState.isOpen) {
    BackHandler {
        scope.launch {
            scaffoldState.drawerState.close()
        }
    }
}
So I guess the issue was (like you suggested) using
enabled
instead of wrapping in a conditional statement
j
Hehe, yeah looks similar to what BackHandler doing under the hood I think. But yeah its important having if blocks to not enable BackHandler when not using it, to prevent issues with other places using it at the same time kind a 😛 Depending in which order they are added ofc.
thank you color 1
If I remember correct its bad using enabled, as it will still having the callback blocking and it will always be called each time recomposition happening 😛 If doing if block will protect it from blocking anything and not part of recomposition loop kind a. Hard to explain. Its very complex to get it right, thats for sure 😄 Very confusing if having like single Activity approach and multiple "screens" compose in parallell and all of them using like navigation icon with Back Handler as of example with bottomappbar. Or combined with TopBar, Gestures, Drawer, BottomSheet and Dialog in parallell 😄
So important to keep track which one active in the screen and make sure all of them sharing same callback or stream them to syncronize and not block each other 😛
👍 1
m
Yeah, I’ll have to do that, because I see that my latest solution doesn’t work after rotating the screen. It looks like voyager is adding itself after the BackHandler does its work.
j
Hehe yeah I dont like frameworks like Voyager and such doing those automagic things in background with androidx lib transitive deps. You bound yourself to much into their solutions. Thats why prefer more lightweight things like Circuit, which sure also doing some magic but not that.
m
Wouldn’t it make more sense to add the nav drawer to the back stack?
j
Depends, maybe. I do that for Bottomsheet, with Circuit content overlay 😄 Or well synhtetic stack kind a.
m
Voyager allows overriding of back behavior
onBackPressed: (Screen) -> Boolean
which allows me to workaround this for now. I’ll revisit when I migrate the compose navigation logic.
👍 1
156 Views