-
Notifications
You must be signed in to change notification settings - Fork 27k
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
Makes Router more accessible to developers #63429
Comments
Could you please create a document, maybe a Medium publication, explaining what were the problems with |
@feinstein That sounds like a good idea. |
Maybe add the Pages API as well, or any other modifications that came along. |
On this topic, I'm trying to create a simple example, but keep getting errors.
I don't see what I'm doing wrong:
routing.dart
|
Hi @esDotDev Page buildNavPage(String name, Widget Function() builder) => CustomBuilderPage(
key: ValueKey(name), routeBuilder: (_, __) => PageRouteBuilder(pageBuilder: (_, __, ___) => builder?.call())); it is complaining the route it return does not set its setting to the page. Page buildNavPage(String name, Widget Function() builder) => CustomBuilderPage(
key: ValueKey(name), routeBuilder: (_, settings) => PageRouteBuilder(settings: settings, pageBuilder: (_, __, ___) => builder?.call())); or just use TransitionBuilderPage We should probably throw a more meaningful message than just assertion error |
Thanks, that works! I can't seem to figure out how to keep the browser url and history in sync though. I've implemented
Browser url never seems to change. I also tried calling Router.navigate() inside the build method of the Delegate, didn't seem to work. I also seem to be getting a null RouteInformation on startup, forcing me to add this to the top of
|
Am I correct in my understanding that, other than the initial call, Additionally, Finally, I don't quite see how we would emulate the previous Navigator.push() behavior, where we could endlessly add to the NavStack and then pop back. This would be analagous to a web user clicking the same 2 links back and forth creating many entries in the back stack. I tried to implement something similar where I add the new pages to a stack, but it's throwing various errors, or just not loading the new pages:
|
Yes that is true for all the platform embeddings that we owned (third party embeddings or plug-ins can do whatever they want)
Yes and no, it will be called in Android. For Web, the backward button currently calls the _handlePopPage. The engine change i am working will change that behavior. After the change if app uses router, the Web will pass the new url and state after the backward/forward buttons to the route information parser, which passes the parsed result to
As I mentioned in previous question, We will separate our the browser history and the router nav stack. Browsers keep tracks of all the urls and states the app navigated to, and just tell the app go to specific url and state when user click backward and forward button. We wouldn't run into this situation. The code you provide is a bit contradicting with our design, your app state |
Oh, that is interesting that browser history will become just a list of intents. But how does that work on Android? Previously we had similar behavior to what you're describing, where we would just push the same route many times and continually add to the stack (think like viewing many I don't understand the one to one mapping thing you are describing. I'm just trying to maintain a history of routes, and let that grow as new routes are added, and then allow them to be popped off. I don't know the routes before hand, for example user might do something like:
My assumption was that I could just manage this one-dimensional page stack myself, provide the pages to the navigator as needed, and then rely on the Navigator to pop the stack. That's not correct I guess? |
Looking at my example, I realize I shouldn't be doing this in build, but instead in the setNewRoutePath. But is there an issue other than that? Like is this not an expected use case:
[Edit] I realize now that the example doesn't really make sense either, since setNewRoutePath() is only called from the OS. So I guess build is the only place this could go? I guess I'm just confused how you would emulate a classic viewstack with the Router, working on both Android and Web with the same behavior... are we forced to use Navigator.Push as opposed to just setting the new Path config? My understanding from what you're describing, is that if I had an app that could generate a long history of routes, on Web, using Router, when I take that same app to Android, I will get different behavior than web in terms of history? ie, Android app would have no history, unless I refactor to use the classic Push API. |
I think I understand what you are saying with the 1:1 mapping. I should have something more like this??
Seems like this would ideally be part of the concrete delegate, so we could just have a delegate with that behavior and re-use it. But I guess we can do the same with a specialized StackableRouteState or something. If this is correct I don't really understand why my example was not working. The seem technically to do the same thing, except mine might create duplicate pages if |
yes Android and web will act differently in terms of route history management, they have very different ways on dealing with history. For web, every history entry is just a page, it does not really stack on each other so that it can go forward and backward. where the android page is pages stack on each other so it can only go backward. On web, you don't worry about route stack, you just need to focus on which page you build in setNewRoutePath when giving a url, and you should not stack another page on top in the method (unless you have a in page button that pop that page then you should probably build multiple page with that url, for example, Gmail). The 1 to 1 mapping I mentioned earlier is that one url should always produce same list of pages. for example You may think then I do I support android in this case? the android route stack is managed some where else, the On android, it is a whole different story. for example open a page on button click, in this case, you WILL want to add that page on top of the existing page. |
for 1:1 mapping you should have something like this Future<void> setNewRoutePath(MyAbstractPageConfig path) async {
state.currentPath = path;
if (state.currentPath is HomePage)
_pages = <Page>[HomePage()];
else if (state.currentPath is HomePage)
_pages = <Page>[HomePage(), SettingPage()];// I build two page because SettingPage may have in page back button or HomePage have state that I want to preserve
} |
Ok, I will try and digest this. My inital thought is that I don't know why we wouldn't abstract these into the same behavior. From the app developers viewpoint, the only difference to us is that one can go forward + back, and the other only back. (And we actually don't care about this either, a page is a page, we will just load it) So in both (all) cases, we really just need a list of history state, and then the ability to re-construct any given route. I as the app developer don't really care how the OS manages these history states( back button, verbal command, direct url link, sign language ), or what it tells me to jump to. I do want some control over what history states are added to this history as I browse, but I don't see why I should be concerned with which sandbox/environment the runtime is in. Back btn is a back btn, app state is app state, routes are routes... seems like an unnecessary distinction. Would we then need an alternate implementation for iOS, and maybe another implementation for Fuschia or some other future OS, where each app needs to handle (and worse, understand) all these os-specific mechanisms for history?
I still don't understand how the classic viewstack would be done on Android/iOS/Desktop if we can not maintain our own dynamic stack of pages, and do not have access to whatever mechanism the Web build is using to maintain this list of router entries. |
I think this is true in the current design, the
I agree with most part, but browser backward button is not the same as android back button. the android back button is more of dismissed the current activity. the browser backward button is to go to previous page that you were viewing, so the backward button can mean pushing a new page and can be confused with flutter's route stack. That is why we want to separate them out to a different concept. Now the android back button is still dismiss the current activity, the browser backward button is treated as I think the design mindset should be make sure You also mentioned your app does not work, Can you be more specific where it doesn't work or provide a runnable example? |
Just feels like it's an os level implementation detail... like from the Developer perspective, both end in a Widget, the only thing we really care about is that, to the extent there are transitions, they play in the correct direction. Which basically comes down to: is this 'new' view behind me in the history stack.
This works for the use case I'm describing, if Pages can be a List of pages rather than a single item... I'll give it a go? |
It looks like you are trying to mix between the page back button with the browser backward button. That means the same url may present different state, you will need to leverage the |
Well in this example, when on Web or Android we would hide our internal back btn, and rely on the OS btn to drive the pop. We wouldn't show both at once. When on Desktop or iOS or PlatformWithoutABackButtonX, we would provide a back btn, this way we get consistent behavior on all platforms, and we don't have two code paths. |
@esDotDev then it will be much simpler, something like this should work. You can't really test web until this is merged flutter/engine#20794. if you can build custom engine, you should be able to test it with the code in that pr. abstract class RoutePath {
const RoutePath();
}
class HomePath extends RoutePath{}
class Page0Path extends RoutePath{}
class Page1Path extends RoutePath{}
class Page2Path extends RoutePath{}
class MyRouteInformationParse extends RouteInformationParser<RoutePath> {
@override
Future<RoutePath> parseRouteInformation(RouteInformation routeInformation) {
if (routeInformation.location == '/')
return SynchronousFuture<RoutePath>(HomePath());
if (routeInformation.location == '/0')
return SynchronousFuture<RoutePath>(Page0Path());
if (routeInformation.location == '/1')
return SynchronousFuture<RoutePath>(Page1Path());
if (routeInformation.location == '/2')
return SynchronousFuture<RoutePath>(Page2Path());
throw '404 ?';
}
@override
RouteInformation restoreRouteInformation(RoutePath configuration) {
if (configuration is HomePath)
return RouteInformation(location: '/');
if (configuration is Page0Path)
return RouteInformation(location: '/0');
if (configuration is Page1Path)
return RouteInformation(location: '/1');
if (configuration is Page2Path)
return RouteInformation(location: '/2');
throw 'unknown route?';
}
}
class MyRouterDelegate extends RouterDelegate<RoutePath> with ChangeNotifier, PopNavigatorRouterDelegateMixin<RoutePath>{
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
List<Page> get pages => _pages;
List<Page> _pages;
set pages(List<Page> pages) {
_pages = pages;
notifyListeners();
}
@override
Future<void> setNewRoutePath(RoutePath configuration) {
_pages = <Page>[HomePage()];
if (configuration is Page0Path)
_pages.add(Page0());
if (configuration is Page1Path)
_pages.add(Page1());
if (configuration is Page2Path)
_pages.add(Page2());
return SynchronousFuture<void>(null);
}
@override
RoutePath get currentConfiguration {
if (_pages.last is HomePage)
return HomePath();
if (_pages.last is Page0)
return Page0Path();
if (_pages.last is Page1)
return Page1Path();
if (_pages.last is Page2)
return Page2Path();
throw 'unknown route?';
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: navigatorKey,
pages: _pages,
onPopPage: _handlePopPage,
),
bottomNavigationBar: Row(
children: [
FlatButton(onPressed: () => pages = List<Page>.from(pages..add(Page0())), child: Text("Product 1)")),
FlatButton(onPressed: () => pages = List<Page>.from(pages..add(Page1())), child: Text("Product 2)")),
FlatButton(onPressed: () => pages = List<Page>.from(pages..add(Page2())), child: Text("Product 3)"))
],
),
);
}
}
class Page0 extends TransitionBuilderPage{
Page0(): super(
pageBuilder: (_, __, ___) => Text('Product 1')
);
}
class Page1 extends TransitionBuilderPage{
Page1(): super(
pageBuilder: (_, __, ___) => Text('Product 2')
);
}
class Page2 extends TransitionBuilderPage{
Page2(): super(
pageBuilder: (_, __, ___) => Text('Product 3')
);
}
class HomePage extends TransitionBuilderPage{
HomePage(): super(
pageBuilder: (_, __, ___) => Text('HomePage')
);
} |
Ah, thanks I missed that.
Well would be nice to hear what different ideas they have been exploring and what the next steps are. |
@Jonas-Sander The API is not likely to change, it is the complexity vs functionality. This is the simplest form we can come up with while still enables all use cases. Therefore, we have being explore different ideas around building a routing library on top of the API that targets specific use case. This should theoretically has simpler API, but we do not have a concrete idea yet. |
@chunhtai Great, thanks for your answer. I have some follow-up questions.
|
Ya I don't totally get this either. Deeplink is obviously a huge priority, and the browser back btn needs to work, but "forward" seems much less important to your avg developer. If "forward" specifically is not important to you, then you really don't need Router... It's very easy to get the browser url updating and to support deep-linking, and also awesome libs like yours that can make that even easier. Not sure, but I think with a little bit of work, you could get those history states working directly with the Imperative API and just sidestep Router all-together. Also, you can still use the state-first paradigm, without using Router. I have a running example here, with code here. I wasn't able to implement I think this could be a fairly key part of the messaging from Google to Developers to make things easier. Explain Router is the "all-in-one" but takes a bit of wiring, but there are other ways to get there as well if you want the "single page app" approach that many will be fine with: No fwd btn, browser-back works as pop(), and deeplinks work). |
Hi, I'm experimenting with my router package on top of Navigator 2.0, which provides concise, cohesive, type-safe API, (hopefully) without losing the functionality and flexibility of Navigator 2.0. |
@chunhtai Curious what is the best way to implement a system where we need an Overlay above the Navigator? For example,
Wrapping with a 2nd Navigator is not great as we have to wrap our content in a Has this use case been considered? Is there some elegant way I'm missing? The Overlay widget itself seems like it would be ideal, but I can find zero guidance on how to implement Overlay in a standalone fashion. I've posted a question on that here: https://stackoverflow.com/questions/66425271/how-to-use-an-overlay-without-the-navigator-app |
The answer to the above, is to return something like this:
Now any persistent UI that wraps the navigator/router, can interact with an Overlay, and things like Tooltips should work, |
@chunhtai One thing I really can't seem to determine from the docs, is how I can simply bind to the current location from within the widget tree. EDIT - I figured it out I think? This seems to work well, but is pretty verbose. Please lmk if there is an easier way than this. One downside to this is it needs to be a child of Router and use Router.of(context), I would prefer to be able to bind directly to the global path value if possible. class _LocationTextState extends State<LocationText> {
RouterDelegate get delegate => Router.of(context).routerDelegate;
RouteInformationProvider? get provider => Router.of(context).routeInformationProvider;
@override
void initState() {
super.initState();
scheduleMicrotask(() {
delegate.addListener(_handleRouteChanged);
provider?.addListener(_handleRouteChanged);
});
}
@override
void dispose() {
delegate.removeListener(_handleRouteChanged);
provider?.removeListener(_handleRouteChanged);
super.dispose();
}
void _handleRouteChanged() => setState(() {});
String getLocation(BuildContext context) {
final r = Router.of(context);
final parser = r.routeInformationParser;
final config = r.routerDelegate.currentConfiguration;
return parser?.restoreRouteInformation(config)?.location ?? '/';
}
@override
Widget build(BuildContext context) => Text(getLocation(context));
} |
@chunhtai Does Flutter team have (or plan to have) an API to delete an entry (not just overwrite the previous one) in the browser history? |
@nguyenxndaidev No, there is no such API, and I think you can't do that in native web too, at least not with other side affect. |
@chunhtai yeah, I think you are right. |
One section of the docs that is quite interesting, but not well explained is the secion on nested routers: I'm struggling to understand how the root router would "making the result available for routers in the subtree". |
Oh yes! I want to know how that works, too. Such a feature would be great for nested navigation scenarios, e.g. if each tab in a TabBarView could have it's own Router and it's own set of routes. |
I've been thinking about this all night, I guess the Router at the top could do something like:
With routers below doing:
I'm not sure if that technique is what the docs are alluding to, or something else. My main question with this approach, is why use a child |
@esDotDev there can be many way to make the result available, you can store the result in app state and let the sub router grabs the result from it.
You can just drop a navigator, but you won't be able to use back button dispatcher. |
Ah, that didn't occur to me, so Router is responsible for listening for OS level back buttons, and without some control over the navigator, the navigator can't respond to those events properly. So we should still use a Router to wrap our child Navigators, so those navigators can response to OS level back events. Looks like some more details here: https://api.flutter.dev/flutter/widgets/BackButtonDispatcher-class.html |
Would love to see the community share their solutions here as well. My solution is to pass the tabs as query arguments.
What really bothers me is I haven't been able to get nested tabs with different URLs instead of arguments (i.e.
Simply put, the Flutter solution of nesting a Navigator inside of a Navigator (multiplying the complexity) is insanity and doesn't work in practice. Creating some dedicated Navigation coordinators and data communication widgets would be much more usable and sane. Perhaps a TabNavigator Widget that allows the user to set URLs for each tab and then updates the parent Navigator with URLs correctly. Edit: Managing back navigation within nested tabs is another issue on top. Sometimes back should navigate within the current Navigator. Other times, it should be discarded and pressing back should navigate on the parent Navigator. |
Specifically the use case I'm interested in, is how to have multiple routers, inside an indexed stack, and switch between them, with some root ancestor widget handling the route parsing. Finding it hard to wrap my head around who is responsible for creating pages, or where the state should live. Add to that the back button dispatcher reference passing and I'm pretty lost. The primary goal is that the page stacks in each Navigator would be stateful, and persist when the other tab is in view. So, imagine an app with only 2 routes ( A demo of this just this would be amazingly helpful. |
This issue is marked P1 but has had no recent status updates. The P1 label indicates high-priority issues that are at the top of the work list. This is the highest priority level a bug can have if it isn't affecting a top-tier customer or breaking the build. Bugs marked P1 are generally actively being worked on unless the assignee is dealing with a P0 bug (or another P1 bug). Issues at this level should be resolved in a matter of months and should have monthly updates on GitHub. Please consider where this bug really falls in our current priorities, and label it or assign it accordingly. This allows people to have a clearer picture of what work is actually planned. Thanks! |
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of |
The router widget is pretty complex because it solved a whole range of issues. We should think about how do we make it easier for developers to use it.
Here are some ideas:
Add more opinionated delegates with a simpler API for common use casesDecided to go with https://github.com/flutter/uxr/wiki/Navigator-2.0-API-Usability-ResearchDocumentation:
The text was updated successfully, but these errors were encountered: