Suppose a screen has two navhosts: NH1 and NH2. NH...
# compose-android
m
Suppose a screen has two navhosts: NH1 and NH2. NH1 is always visible, NH2 is only sometimes visible (alongside NH1), and when it is, should always receive back events as priority. In other words, the user can continue hitting back through NH2 backstack until it disappears, and only then will the back events start taking effect on NH1. To achieve this, I add a
OnBackPressedCallback
to the NH2 composable and it’s only enabled when NH2 is visible. This works fine except for the fact the predictive animations don’t work if you just override
handleOnBackPressed
. I know you can just implement functions like
handleOnBackProgressed
but it would be much better to reuse existing logic (which must already be in the androidx activity library) instead. Any pointers?
👀 1
s
Why do you need to add a
OnBackPressedCallback
yourself at all? NavHost itself has a
PredictiveBackHandler
internally which handles this for you.
And I suppose for the sake of this question we should skip asking why you need 2 NavHosts anyway? 😄 Because many times when these topics are brought up, the answer is to just skip the second NavHost and everything works properly automatically.
m
When NH2 is visible, sometimes NH1 backstack is pushed to. By default, the back handling will pop in the reverse order they were pushed which I don’t want. The workaround is to use custom back handling only when NH2 is visible, so that NH2 pop is prioritised.
The only way I can think it would work using a single nav host is if the above-mentioned NH1 backstack pushes were somehow pushed underneath the NH2 backstack events.
Maybe it helps if I describe what is going on. NH1 shows search results (among other things). NH2 shows a handwriting pane (amongst other things). The user can bring up the handwriting pane (alongside search results) and as they write something, the search results are pushed to NH1. Back should pop the handwriting pane, not the search results.
I had another thought. Is there any way to detect which NavHost is due to receive the next back event? I guess that is the same as asking which NavHost received the last push event.
s
Yeah BackHandlers are added on a stack on top of each other afaik. So it's a matter of which one was added last. I will admit I'm having a hard time visualizing your use case to understand what you're doing here. If your back presses sometimes pop from the right pane and sometimes from the left one, doesn't the entire experience feel hard to predict what your back press will do next?
m
Possibly, yes. But it makes sense that once you have finished using the handwriting pane to enter the search text, you want to dismiss the handwriting pane, not the search results. The thing here is that the two nav hosts are visually at the same level, though I suppose that’s a good argument to make the handwriting pane appear at a higher (z-axis) so that it then makes more sense the Back would apply to that first. BTW, when you say BackHandler, do you mean backstack entry?
m
Right so with two NavHosts, by default, there are two PredictiveBackHandlers. But I’m pretty sure the back events (across nav hosts) are occurring in reverse chronological order, rather than simply going through the backstack of one NavHost and then the other.
s
Hm, really not sure why it'd have this interaction tbh 😅
m
Wouldn’t the user expect to go back from the previously navigated to screen?
s
I will admit that I am not sure what a user would expect there as I don't think I've ever used an app that has done something like what you describe. It probably has to do with the fact that I don't have such big screen devices myself. I would hope there would be some guidance here somewhere in the docs about providing good user experiences in bigger screens
m
Hmm, so it seems the actual default behavior is to do with the order in which the NavHosts (i.e. PredictiveBackHandlers) are added, as you mentioned earlier. I think I’ve seen another post where @Ian Lake mentions this. This is kind of unintuitive in the case where you have two NavHosts visible at the same time on a screen, because, say, if I have a column of NH2 and then NH1 but I want NH2 to take priority, I have to manipulate it so that NH2 is added last? Any ideas how to do that? Add the two items of the column in reverse order but maintaining original layout?
This hack seems to work:
Copy code
var mainNavHostAdded by remember {
    mutableStateOf(false)
}
Column(modifier = modifier) {
    appBar()
    if (mainNavHostAdded) {
        otherNavHost()
    }
    mainNavHost()
    mainNavHostAdded = true
}
BTW, is this hack safe, or should
mainNavHostAdded
be set in something like
onGloballyPositioned()
? Also, would it be a mistake to use
rememberSaveable
?
Copy code
var mainNavHostAdded by remember {
    mutableStateOf(false)
}
Column(modifier = modifier) {
    appBar()
    if (mainNavHostAdded) {
        otherNavHost()
    }
    Box(modifier = Modifier.onGloballyPositioned { mainNavHostAdded = true }) {
        mainNavHost()
    }
}
s
Why are you using onGloballyPositioned to do a side effect instead of the side effect APIs?
m
That’s my question. Which way is best?
SideEffect
? And what is the technical reason why the hack of just putting
mainNavHostAdded = true
directly in the composable, shouldn’t be done? If using
SideEffect
where should it go? Before, after or inside the
Column
, or it doesn’t matter?
s
If it's a thing you want to happen once but on the next frame, it's
LaunchedEffect(Unit)
Regarding why you don't want to write to a mutable state in composition after it was read, the reason is described here https://developer.android.com/develop/ui/compose/performance/bestpractices#avoid-backwards
A way to avoid this entire problem of putting things in the wrong order just so your composables are added in composition in the order you want your BackHandlers to be put in, would be to make a custom layout, put the NavHosts in the order they must be, and then simply place them one under the other as you wish. Something like
Copy code
Layout(
  {
    AppBar()
    MainNavHost()
    OtherNavHost()
  }
) {
  ...measure
  layout() {
    appbar.place()
    otherNavHost.place()
    mainNavHost.place()
  }
}
So that they are added in composition in the order you wish, but they are rendered in the order that they must. Here are some docs on custom layouts.
m
Thanks very much, that’s very helpful. I’m a bit hesitant to use a custom layout though, as it brings more complexity than my existing solution. It’s a case of weighing up simple hack vs relative complex proper solution. How about the
rememberSaveable
part? I think it’s important to only use
remember
otherwise screen rotation would incorrectly lead to the other host being added before the main host.