Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pressing back via AppBar or System gesture pops to root on Android #176

Open
Maikuh opened this issue Jul 16, 2021 · 24 comments
Open

Pressing back via AppBar or System gesture pops to root on Android #176

Maikuh opened this issue Jul 16, 2021 · 24 comments
Milestone

Comments

@Maikuh
Copy link

Maikuh commented Jul 16, 2021

Expected Behavior

  1. On /, push to /second
  2. On /second, push to /third
  3. On /third, pop goes to /second
  4. On /second, pop goes to /

Actual Behavior

  1. On /, push to /second
  2. On /second, push to /third
  3. On /third, pop goes to /

Notes

This doesn't happen if /third is relative to /second, like /second/third. Also, on the Web, going back via the Browser's back button has the expected behavior too. I have a feeling it has to do with https://github.com/tomgilder/routemaster/wiki/Routemaster-Flutter-scenarios#skipping-stacks But I couldn't see a way to disable that behavior.

Minimal reproduction code

import 'package:flutter/material.dart';
import 'package:routemaster/routemaster.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);

  final routes = RouteMap(routes: {
    '/': (_) => MaterialPage(child: FirstScreen()),
    '/second': (_) => MaterialPage(child: SecondScreen()),
    '/third': (_) => MaterialPage(child: ThirdScreen()),
  });

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: RoutemasterParser(),
      routerDelegate: RoutemasterDelegate(routesBuilder: (context) => routes),
    );
  }
}

class FirstScreen extends StatelessWidget {
  const FirstScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Routemaster.of(context).push('/second');
          },
          child: Text('Go to second'),
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  const SecondScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Routemaster.of(context).push('/third');
          },
          child: Text('Go to third'),
        ),
      ),
    );
  }
}

class ThirdScreen extends StatelessWidget {
  const ThirdScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Third Screen'),
      ),
      body: Center(
        child: Text('The third one'),
      ),
    );
  }
}
@tomgilder
Copy link
Owner

tomgilder commented Jul 17, 2021

Thanks for the issue and reproduction!

And... welcome to the hell of Android back buttons. It's a world of confusion.

Just to clarify definitions:

  • The back button is the Android system back button, at the bottom of the screen
  • The up button is the other "back" button within your app, normally top-left in the App Bar

Routemaster's behaviour is, I believe, according to the Android docs, the "correct" behaviour on Android:

"Within your app's task, the Up and Back buttons behave identically."

According to Google, within your app, the back and up buttons should always do the same thing. This is different to a web browser - the back button always navigates backwards through the stack of URLs, ignoring the app structure.

So Routemaster's behaviour matches this, so the back button navigates upwards through the URL/page hierarchy, rather than what the user has actually seen (as on the web).

But Google's guidelines on this have changed; they used to say that the system back button should follow your expected behaviour, and do something different to the up button. And some apps follow the old behaviour. Even Google's own Android apps have inconsistent back button behaviour.

Soooooo yes... it's really confusing. I'm open to any suggestions to add options here, as long as they don't really overcomplicate the API.

There are, however, already several ways you could override this, such as using BackButtonDispatcher or overriding RoutemasterDelegate.popRoute.

@sp00ne
Copy link

sp00ne commented Aug 4, 2021

Correct me if I'm wrong, but isn't that's exactly what @Maikuh is stating? That it is not following the behaviour according to the docs?

"Within your app's task, the Up and Back buttons behave identically."

Under the "Actual Behavior" he states that navigating to the third screen and having second one in the "back stack", still pops to root

@josh-burton
Copy link

Are there any updates on this one? Standard behaviour these days is for both the system back and app back button to behave the same, and just pop back one route.

@tomgilder
Copy link
Owner

@josh-burton what do you mean by "pop back one route"? An upwards or reverse-chronological pop?

@josh-burton
Copy link

reverse-chronological.

@tomgilder
Copy link
Owner

I'm very much open to API proposals on this.

However I feel like different developers will expect different behaviours on different platforms. This makes it pretty complicated.

Just to define terms:

  • Reverse-chronological: reverse navigation which goes back to the screen the user last saw, no matter where it is in the app
  • Upward: navigates up the current navigation hierarchy, potentially showing a screen the user has never seen.

This is discussed on page 34 of the recent Flutter routing usability report.

Currently the only reverse-chronological navigation with Routemaster is on web browsers, with the browser's back button. Any default app bar back button will use upward navigation. You can't currently do this on Android, but I'd like to enable it.

Apparently I misunderstood the Android guidelines, and every back button should always be reverse-chronological. But this is surprising to me, and not how a lot of apps I've seen work, and against my expectations (but I'm not an Android user).

But an interesting problem is the expected behaviour can differ between platforms. For instance, on iOS there's only ever "up" navigation. If the entire navigation stack is replaced, in general there's no way to return to the previous stack.

So here are some questions we need to answer:

  • What should the default app bar back button behaviour be?
  • What should the default Android system back button behaviour be?
  • Should any of the default behaviour differ between platforms?
  • Should either (or both) be customisable? If so, how?
  • What should Routemaster.of(context).pop() do by default?
  • What other APIs should Routemaster provide to achieve upward or reverse-chronological navigation?
  • Is it acceptable to make breaking changes to this behaviour?

Also, if anyone has a good idea for a simpler term for "reverse-chronological", I'd love to know it 😁

@tomgilder tomgilder added this to the 0.11.0 milestone Aug 27, 2021
@josh-burton
Copy link

Thanks for the great explanation. For me I think it makes sense for both iOS and Android to use up navigation by default.

And the option to customise per platform, or for all platforms would be great as I'm sure there will developers who require it.

@buzzware
Copy link

buzzware commented Sep 8, 2021

This makes me sad, I've been loving Routemaster up to this point.
I always expect "back" on every platform to be reverse-chronological, mainly because its not like the user has a map of the routes in front of them - its an internal developer concept.
I agree with the OP, and I'm not sure how to implement the "expected" behaviour.

@buzzware
Copy link

buzzware commented Sep 8, 2021

I'm on /feed, and after pushing /settings see in the data structures that the page stack has two entries, and it still does when Settings is built, but when the back button is tapped, the stack only has one entry, for the current page, so there is no way to return to /feed. This is on iOS.
image
image

@mdrideout
Copy link

I too was suddently perplexed when I found that the app bar back button was skipping the entire history and going back to the app root path.

To quote OP:

Expected Behavior

  1. On /, push to /second
  2. On /second, push to /third
  3. On /third, pop goes to /second
  4. On /second, pop goes to /

Actual Behavior

  1. On /, push to /second
  2. On /second, push to /third
  3. On /third, pop goes to /

I have only ever experienced the expected behavior (either when developing or as a consumer using apps) and I have never experienced what routemaster has implemented as actual behavior, where the stack is skipped. This includes flutter web on navigator 1.0, where the app bar back button works just like a browser back button.

To skip back further, I've always just used popUntil I hit the named route.

Question: Is there anyway to get the app bar back button to navigate in reverse chronological order, like the "expected behavior" op mentioned?

Otherwise, how do you recommend one implement that sort of functionality. For example, I navigate users to page /third, and it's a dead end purposefully, using the back button is intentionally the only option to immediately go back to the previous screen.

@mdrideout
Copy link

I want to follow up and provide a specific example, because I realize that what I want is possible... but it becomes a nightmare of origin management.

Example

My apps are cross platform for web, ios, and android. So when I say back, I expect the browser back button, the app bar back button, and the android back button to function the same way (like they do on the twitter app / twitter web)

  1. I am on screen /business and I want to click a job item to view it's details.
  2. I use Routemaster.of(context).push('/job/' + index.toString()); to push to the job screen to view it's details.
  3. When I am done, I hit back. But instead of winding up back on the business page, I wind up on the app root.

In that situation, the job page's route looks as follows:

'/business': (_) => MaterialPage(child: BusinessDashboardScreen()),
'/job/:jobLogIndex': (route) =>
          MaterialPage(child: JobDetailScreen(jobLogIndex: route.pathParameters['jobLogIndex'])),

To fix this issue as routemaster currently works, I can change the job to a "relative" link.

  1. I am on screen /business and I want to click a job item to view it's details.
  2. I use Routemaster.of(context).push('job/' + index.toString()); to push to the job screen to view it's details.
  3. When I am done, I hit back. Because it was a relative link, I am returned to the /business page

In this situation, the routes look at follows:

  '/business': (_) => MaterialPage(child: BusinessDashboardScreen()),
  '/business/job/:jobLogIndex': (route) =>
      MaterialPage(child: JobDetailScreen(jobLogIndex: route.pathParameters['jobLogIndex'])),

Foreseen Problem

The problem I have with this is that I need to be able to get to the job screen from several different pages. So if this style of navigation only seems to work if I create relative job paths for every potential screen that can link to the jobs page. So if I have 5 screens that link to the jobs page, I need to create additional relative jobs routes in the RouteMap.

If I want to get to the job screen from the "monitoring" screen, I have to create the route again with that prefix.

  '/monitoring': (_) => MaterialPage(child: MonitoringDashboardScreen()),
  '/monitoring/job/:jobLogIndex': (route) =>
      MaterialPage(child: JobDetailScreen(jobLogIndex: route.pathParameters['jobLogIndex'])),
  '/business': (_) => MaterialPage(child: BusinessDashboardScreen()),
  '/business/job/:jobLogIndex': (route) =>
      MaterialPage(child: JobDetailScreen(jobLogIndex: route.pathParameters['jobLogIndex'])),

As an app grows, this does not scale.

Am I missing another solution or way of using this?

@hampsterx
Copy link

on a tangent a bit but I have the same problem above with a detail screen being show from two different list screens. Additionally I am using TabPage but the detail screen should not show the tab bar, not sure thats possible currently but it's a very common pattern.

In my situation I don't mind to define the routing twice as we can provide urls in emails consistent across web and mobile (deep links). If I am going further down into a sub screen I will 'push' otherwise 'replace'. If I have deep linked in I would expect back to exit the app not go up.

If I navigate down a nested route then either push or replace would achieve the same thing, that feels weird too.

@tomgilder
Copy link
Owner

@buzzware nooo! Let's make you happy again! Do you have any comments/ideas on my questions from above?

@mdrideout thank you so much for the specific example, that's really useful.

I'm thinking about the API for this. I don't think the actual coding here is that difficult, we can solve this together - it's just designing the API, making it flexible enough for everyone's needs but not making it overly complicated is quite a big challenge.

I'll see if I can come up with a way you can work-around this until the functionality is in Routemaster.

I totally get why this is a big issue and want to address it ASAP.

Who would have thought something as simple as a back button is so complicated? 😁

@tomgilder
Copy link
Owner

Okay, I've done some thinking! Current thoughts are below.

I'm prioritising this issue above anything else, I understand how important it is.

I would massively appreciate feedback. Designing APIs alone is really hard 😁

API

  • Add a Routemaster.of(context).back() method. This would do reverse-chronological navigation.
  • Routemaster.of(context).pop() would remain as it is. Having back and pop might be a bit confusing, but I can't think of better names.
  • Add an option to RoutemasterDelegate for the default back button behaviour
    • e.g. RoutemasterDelegate(backBehavior: BackBehavior.reverseChronological)
    • Really don't like that name currently. Suggestions warmly welcomed.
    • This would affect both the system back button and up buttons.
  • No breaking changes for now. Possibly make a breaking change in a future version, but with a lot of warning - it could horribly break existing apps.

Scenarios

Back button reverse-chronological, but up button pops

This would be achievable by using the global BackBehavior.reverseChronological but providing a custom back button to the app bar: BackButton(onPressed: () => Routemaster.of(context).pop())

Back button delegate

There should be a way to intercept the Android back button and make decisions based on app state. This could include going to some other route that isn't in the back stack at all.

Complications

Just Android

None of this would affect web. Web is totally driven by the current URL, nothing else. This makes APIs potentially very confusing. I was totally confused by this at first with Flutter's BackButtonDispatcher - I expected it to work on the web, but it's Android-only.

But... I really don't want to add parameters like androidBackBehavior. It's totally possible future platforms like Fuchsia will have back button support.

I think the only answer to this is clear documentation.

Tabs

Another thing - should reverse-chronological navigate back through tabs? For example:

  1. You open the app, starts on tab one
  2. Switch to tab two
  3. Press back button

Should the app switch back to tab one, or close (because there's nothing in the back stack)? Different Android apps do different things. This also feels like something that should be configurable?

In summary... PLEASE HELP ME, BACK BUTTONS ARE MAKING ME CRAZY 😁😁😁

@mdrideout
Copy link

I think this sounds great.

  • API: I think that looks perfect. I would definitely use RoutemasterDelegate(backBehavior: BackBehavior.reverseChronological) myself.
  • Android Back Button: I have personally not experienced the Android back button working differently from a browser back button. But if there is an option to override the android back button for different behavior, I think that is good as there may be use cases.
  • Tabs: If it's possible, tabs have the option to support reverse chronological as well. I think this might be more important for users who are programmatically hijacking tabs UI (I have done this before to use a Listview to display tabs vertically). I think it will be common to want to not go reverseChronological through the tabs.

Couple Questions

  • Do you know how this will impact the RouteMap() from my comment above, making it easier for many screens to link to the same screen with reverseChronological back button behavior?
  • Do you have an option for something similar to popUntil in case the developer wants the back button to skip to the first tab or pop until a named route is reached? This was something I used quite a bit in navigator 1 with various stacks of niche UI screens and overlays.

Name Parameter
not sure if relevant but... I am making use of the "name" parameter because it's necessary for FirebaseAnalyticsObserver This is because my URL's may have path variables that I do not want in analytics (because then a screen would show up as a different screen every time the variable was different, so I need to be able to report to analytics a non-variable-specific name for the screen.

@tomgilder
Copy link
Owner

  • Tabs: If it's possible, tabs have the option to support reverse chronological as well. I think this might be more important for users who are programmatically hijacking tabs UI (I have done this before to use a Listview to display tabs vertically). I think it will be common to want to not go reverseChronological through the tabs.

Yep, think I'm going to need to provide both options for tabs. I asked on Twitter what people thought the behaviour should be, and after 71 votes it's almost 50/50 😁

  • Do you know how this will impact the RouteMap() from my comment above, making it easier for many screens to link to the same screen with reverseChronological back button behavior?

You'll be able to solve that wit this, by having /jobs/:id and always using .back() for app bar up buttons.

  • Do you have an option for something similar to popUntil in case the developer wants the back button to skip to the first tab or pop until a named route is reached? This was something I used quite a bit in navigator 1 with various stacks of niche UI screens and overlays.

Yes, this could be added, but I think I'll leave it just for the moment to get the core right.

Name Parameter
not sure if relevant but... I am making use of the "name" parameter because it's necessary for FirebaseAnalyticsObserver This is because my URL's may have path variables that I do not want in analytics (because then a screen would show up as a different screen every time the variable was different, so I need to be able to report to analytics a non-variable-specific name for the screen.

RouteData already has a property designed for this: pathTemplate. For instance, for path /jobs/123 it'll be /jobs/:id.

@tomgilder
Copy link
Owner

Working on this today. Everyone seems to have a different view on the "correct" Android back button behaviour:

Screenshot 2021-09-25 at 14 38 39

Nightmare. So the only option here is, well, providing options. And possibly lots of them.

Tabs

I'm currently trying to come up with a good API for specifying Android back button behaviour with tabs. Initially I thought there were only two options:

  1. On pressing another tab, an entry is added to the reverse-chronological back stack so pressing back goes to the previous tab.
  2. No back stack entry is added, so the back button never navigates between tabs.

But then it turns out React Navigation has five (!) different options.

Which of these do you think it's useful to support? Would it make sense to add some sort of delegate handler to make decisions?

  1. firstRoute - return to the first tab (default)
  2. initialRoute - return to initial tab
  3. order - return to previous tab (in the order they are shown in the tab bar)
  4. history - return to last visited tab
  5. none - do not handle back button

@mdrideout
Copy link

mdrideout commented Sep 25, 2021

There are also so many different "situations" tabs are used that I think it should not be a global setting for all tabs, but specific to the instance of the tabs declared for a UI scenario.

I'm not sure we need all of these to start with, but I think the 1 & 2 you originally identified are more important than all the options that React Navigation handles, which could be incorporated in the future if the architecture is there to support adding more options.

Dev Scenarios

  1. If you have your tabs as "steps" for something, you may back to handle reverse chronological stepping backward through the tab steps.
  • Main Screen -> Tab Screen Tab 1 -> Tab 2 -> Tab 3 -> Tab 4 -> Tab 5 -> Tab 6
  • If back button went Tab 6 -> Tab 5 -> Tab 4 -> Tab 3 -> Tab 2 -> Tab 1...
  • BUT if the user is on Tab 5, selects Tab 3 from the tab bar, should the back button go to back to tab 5, or to tab 2... So I guess this is a case where those additional React Navigation scenarios helps, because the dev might want to declare that back on tab 3 always goes to tab 2.
  1. If you have two tabs that the user is clicking between for different views of something (potentially clicking between them several times for comparisons, or different chat windows, or different data update screens), having the back button reverse chronological through those two tabs would be ridiculous, and the dev would want to declare those tabs to just to back to the previous screen potentially.
  • User nav scenario: Main Screen -> Tab Screen Tab 1 -> Tab 2 -> Tab 1 -> Tab 2 -> Tab 1 -> Tab 2 -> Tab 1 -> Tab 2 -> Tab 1
  • If back button went Tab 2 -> Tab 1 -> Tab 2 -> Tab 1 -> Tab 2 -> Tab 1 -> Tab 2 -> Tab 1... that would be nuts. But this handling may be unique to only this tab group.

@rydmike
Copy link

rydmike commented Oct 4, 2021

The Unbearable Lightness of the Android Back Button

I started looking at what "big" (well some) apps do on Android on bottom nav or main nav pattern it uses, when using system back button or back swipe function. I had not though so much about it before, but gosh did I find a can of worms.

1. Always pops entire app, regardless of where you are on bottom destination

  • Philips Hue (made with Flutter)
  • DropBox, but persists destination so even fully terminated it starts on last one again
  • MS apps like Word, Excel, PowerPoint

2. If not on 1st destination, goes to 1st, then pops app.

When on 1st destination, these always pops the app, but only from 1st one.

  • Twitter
  • WhatsApp. It uses a top tab view, you can swipe between them, but a back swipe is different and that first takes to main chats tab if you are not on it, and after that pops the app.

I used this in an app, because I disliked 1 so much and this was simple to add. Just a, if not on 1, then move there if the app tries to pop itself, if on one, then fine go ahead pop app.

3. Keep unique visited destinations on a stack equal to nr of bottom destinations, pop back in last used unique order, when nothing left, if not on 1, then back to 1, then pop app

  • GMail
  • YouTube
  • IG
  • Reddit
  • Google Playstore

To mention a few. I rather like this one actually, but 2 is fine as well imo.

4. Keep N unique destinations, then pop the app.

Usually N is equal to nr of bottom destination. Pop them until nothing remains, then pop app. The app can thus be popped from other than 1st destination, depends on how you navigated on bottom navbar

  • Netflix - I think this is just a weird botched version of 3. Did not see anybody else doing this yet.

5. Keep entire bottom nav history.

Just keep them all on a stack and pop back in order until nothing remains, then pop app. Since you started on 1, you eventually end up popping the app from 1.

  • Spotify - Maybe it is not "infinite", but it is pretty deep.

Have you found any other patterns?

Why use back button/swipe on Android with bottom navbars?

Some claim they never navigate with the back button/swipe on Android in apps with bottom navbars.

I do it all the time. Why?

When I hold the phone with one handed grip with my right hand in the lower right corner, I can't reach the 1st destination with my thumb without using very uncomfortable and wobbly phone tilting action to reach it, so I just swipe back with my thumb to get there. I expect it to get to 1st destination at some point, like in cases 2, 3 and 5.

If the app then does pattern 1) it is really the worst UX of them all. Hue that I use a lot makes me wanna scream in frustration. Case 4 is just weird, but you might not notice its weirdness unless you really look for it and create a back stack that shows it, but when it happens it is a bit of a WTF moment.

@tomgilder tomgilder modified the milestones: 0.11.0, 0.10.0 Oct 6, 2021
@tomgilder
Copy link
Owner

tomgilder commented Oct 6, 2021

Hello patient people! I've just done a new prerelease that I believe resolves the core of this issue: v0.10.0-dev4.

It switches the default system back button behaviour on Android to reverse-chronological. I decided to make this a breaking change.

Currently, there's no parameter to switch back to the old behaviour. I'm still thinking about the best way of doing this, and if it's needed. However you can override popRoute on RoutemasterDelegate with return pop(); will revert to the old behaviour.

Tabs

Tab pages default to not adding to the chronological history stack. This is also a breaking change, you can make them add to the history by setting backBehavior: TabBackBehavior.history in the page constructor.

This might not be the best default on Android... but then again I don't think there's any default that will please everyone 😁. I plan to add more tab history options, such as the ones Mike highlighted.

History

There is also a new history object, allowing you to navigate chronologically:

  • Routemaster.of(context).history.back()
  • Routemaster.of(context).history.forward()
  • Routemaster.of(context).history.canGoBack
  • Routemaster.of(context).history.canGoForward

Adding .history makes it a little verbose, but I think it makes it clearer, and it also gives an opportunity to add more history functionality in the future.


I would love feedback and testing on these changes. There is a high chance of bugs, this has required some pretty major changes (40 changed files with 1,661 additions and 233 deletions!). Note that you must use at least the current Flutter stable 2.5 with the new version.

@HelenaSewell
Copy link

HelenaSewell commented Oct 20, 2021

Thanks for including this feature, it's very useful! I tried out 0.10.0-dev5 and strangely it seems to work opposite to how you described above. The default back behaviour was to go back to root, and any times I had used pop it went back one step. I overrode popRoute which you suggested as a way of getting back the old behaviour, and got the behaviour of going back by 1 instead. I tested a bit more and that seemed to be only a modal bottom sheet, not defined in the routing file, which went back to the root when not overriding popRoute. The other route, which did go back 1 at a time, had the format /page1/page2/page3. Both routes at the /page1 level are in tabs.
However, I'm still baffled by overriding popRoute with pop actually going back 1 step at a time.

@zombie6888
Copy link

zombie6888 commented Nov 4, 2021

Thanks for including this feature, it's very useful! I tried out 0.10.0-dev5 and strangely it seems to work opposite to how you described above. The default back behaviour was to go back to root, and any times I had used pop it went back one

It works, you just need to add option for tabs and use history.back() instead of pop()

I think, it would be nice to add this to docs. The feature is very useful. I have app for web, android and ios and expect same navigation behavior for all platforms.

The only bug i faced is when i press back too quickly (by using android/appbar button or gestures) i get an error and navigation breaks. It happens when the history stack contains about 5 or more routes and i tap or swipe very quickly, as a regular user will never do. It was reproduced in android debug mode, i will check it on ios and release build later, then provide more details if you need

updated: The bug was reproduced on release buld too. It's showing tab content of inactive tab, probably previous. Looks like getting params from previous route causing this error:

image

@vbuberen
Copy link

Not directly related to the ongoing discussion, but looking forward to get proper pop like back navigation.
Added Routemaster to a new project a few days ago and was quite surprised to observe the same behavior as the issue author described.

@tomgilder tomgilder modified the milestones: 0.10.0, 0.11.0 Mar 12, 2022
@blexo
Copy link

blexo commented Dec 1, 2022

I'm years late to this discussion... but still - Thank You!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests