-
Notifications
You must be signed in to change notification settings - Fork 27
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
Clarify Nested Routing scenario #35
Comments
Here's an AngularDart example of the first two scenarios. Route paths are defined as a tree: static final articles = RoutePath(
path: 'articles', parent: app.RoutePaths.home, useAsDefault: true);
static final users = RoutePath(path: 'users', parent: app.RoutePaths.home);
And each component can use the <p>home</p>
<nav>
<a [routerLink]="RoutePaths.articles.toUrl()"
[routerLinkActive]="'active'">Articles</a>
<a [routerLink]="RoutePaths.users.toUrl()"
[routerLinkActive]="'active'">Users</a>
</nav>
<router-outlet [routes]="HomeComponent.routes"></router-outlet>
The class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
Text('Home'),
Expanded(
child: VRouteElementData.of(context).vChild ?? Placeholder()),
],
),
),
);
}
}
|
Thanks for dedicating an issue to this. Nesting is so important in Flutter I think this is something we must take time to consider. Concerning what you described: Hierarchical RoutingI think this is interesting, not as a new feature, but really something which makes development easier. Here is an example (using Solution 1, any VRouter(
routes: [
VWidget(
path: '/home',
widget: HomeScreen(),
stackedRoutes: [
VWidget(path: '/home/articles', widget: ArticlesScreen()),
VWidget(path: '/home/users', widget: UsersScreen()),
],
),
],
)
Solution 2, in VRouter(
routes: [
VWidget(
path: '/home',
widget: HomeScreen(),
stackedRoutes: [
VWidget(path: 'articles', widget: ArticlesScreen()),
VWidget(path: 'users', widget: UsersScreen()),
],
),
],
)
Something interesting to note is that this can be taken to the next level. The issue with the example above in that if someone developed This is something that Reach router does really well, I encourage anyone to read the Large Scale Apps sections, showing that not only the path but also the links are relative. This allows true separation of work. Pushing relative path is allowed in VRouter but not to the extend that I wish, anyway this is not the place to discuss my future plans but I hope that you get my point. Router View / Router Outlets AND Nested StacksFirst, I don't really understand the difference between the two. If any, I would say that That being said, I think this is one of the most important topic with Flutter navigation. Flutter loves scoped models and nesting while navigation is inherently global (and least in the web where there is a single path). Re-conciliating is one of the greatest challenges of any navigation package imo. Based on my experience, this example showing a BottomNavigationBar with an inner Navigator managing a stack is a though challenge that should be a really important scenario to conciser (since basically any mobile app faces it) |
Showing nested navigator is easy but it becomes a very complicated topic when we need to support deep-linking and URL on the web. The more time I spend to work with nested routing, the more problems I see on this topic. It has many edge cases to consider:
While there'are technical difficulties, we still need to maintain the architecture to make sure we can easily split our works to multiple teams. This point is a very important thing to me. btw, @lulupointu, Large Scale Apps is an useful link. |
@lulupointu
You suggested both scenarios are important and we should probably have both when we do the usability analysis. @nguyenxndaidev on the other hand suggest on the Router View / Router Outlets scenario, it is also important we analysis the ability to to split the works across teams. To combine both feedbacks, I we were to cover all use cases we have so far. There are two different categories that both focusing on building app in large scale across multiple teams.
I think (2) is the most tricky one, and it is likely if we cover (2), we will also cover (1) What do you think? |
@chunhtai generally, sound good to me. |
Same! |
I'm concerned that we could have many speculative edge cases that push the API design to be more complex than it needs to be for the majority of Flutter users. Adding support for such modularity in the routing system is unlikely to be free of a usability cost. If anyone is interested in exploring this, comparing API design with and without this requirement could be illuminating. |
@InMatrix concerning scalability without usability cost, allow me to tell you what I want to achieve with Here is what one could do final settingsRouter = VRouter(
routes: [
VWidget(path: '/basic', widget: BasicSettingsScreen()),
VWidget(path: '/network', widget: NetworkSettingsScreen()),
VWidget(path: '/advanced', widget: AdvancedSettingsScreen()),
],
);
final loginRouter = VRouter(
routes: [
VWidget(path: '/login', widget: LoginScreen()),
],
);
final mainRouter = VRouter(
routes: [
loginRouter,
VNester(
path: '/settings',
widgetBuilder: (child) => SettingsScreen(child),
nestedRoutes: [settingsRouter],
),
]
);
The goals here are that:
Something interesting to note here is that using this implementation provides no breaking changes to the current
I don't think this has to be true. This example is using |
@InMatrix In It means that, using In case, the child So, at the end, |
Hi folks, |
I don't totally get the team aspect tbh, any routing solution should be able to create testable widgets where the params are injected right into the pages: The best real-world example of a multi-platform app with complex nesting scenarios I can find is actually https://twitter.com/. Surfing through there will pretty much generate every combination of nesting you would need to support in most apps. If you compare their website to their app it is pretty consistent. It breaks down into basically:
That's not every use case out there, but it's pretty close! On Android the tabs do a really nice job of maintaining state/scroll position as you switch between them. On web they kinda bailed on that concept (we can do better!) |
@lulupointu @nguyenxndaidev Thank you both for your explanation! It does seem to be doable. |
Been digging into this a lot more, and some thoughts... There is a rather critical high level things that can drastically change what we mean by "nested routers".
I think 1 and 2 are really pretty trivial, and will not really be a challenge for any routing solution. 3 is the holy grail, and 3 is kinda hard to pull off in Flutter. I don't think many, if any, packages can actually handle it. I think this is the bar we should be chasing. If we can bring that paradigm to the web, of a full stateful app, it will be very impressive. I have a very ugly example, but it works:
qbVU4xuBM4.mp4This is constructed declaratively, from code looking like:
It's all driven by a single My plan right now with this is to release a |
Just a quick small update, I've updated the "persistent navigator" prototype to support history, so we can easily go "back". This solves most use cases needed for "pop", and also gets us one consistent nav paradigm we can think of across all platforms. The one use case not supported is getting results from the popped route, but this can be done using Dialogs, or hoisting view state. F2mn6FEx0t.mp4The back button here looks like:
Regular links look like:
I think when I add a NavStack wrapper around PathStack, we can provide convenience methods like This was made by using 3 nested path stacks, full screen routes site in the root stack, while routes wrapped by the main app scaffold set in the 2nd stack, under that all the Settings views are wrapped in a 3rd stack:
|
@esDotDev Thank you for your investigation here. @johnpryan and a few others working on this project discussed this "offline" last week, and IIRC, we agreed that we need a separate Router for each layer of routes in a nested routing scenario in order to support 1) transition animations in each nested layer, and 2) preservation of state in each nested view/page. Our current storyboard for the nested routing scenario does imply state preservation (e.g., when the user presses the browser's back button on Screen 4, they'd go back Screen 3 instead of Screen 2). Is there any change you'd suggest we make to the storyboard to make the requirements clearer? |
We could have multiple nested routes scenarios for isolated problems. After spent a lot of time for different problems with nested routes. I think this feature will influence the final decision when developers choosing a solution for them. |
A couple things that could be added here are:
3AyeKKoePO.mp4The first 3 are handled automatically if the But maybe there is a way to piggyback on the new Restoration API to do something like this at the Widget level instead of the Router level... in which case it wouldn't be important to retain the StatefulElement, but to rather just define appropriate storage buckets for each stateful-route and they would restore themselves. This would be slower, and use more CPU, but it would save RAM. |
I've created an issue for StateRestoration at runtime, which would likely mitigate much of these issues and remove the need for a Router to try and maintain state for it's children, or at least give an alternative approach that is not a ton of boilerplate. |
@nguyenxndaidev So there might be a way to have a single scenario with a base version and a few advanced ones. The base version could show a tabbed UI within each page (e.g., the tabs within each books section in the current UXR storyboard). This base version should include customizable transition animations between tabs. A slightly more advanced version would preserve tab selections when switching between pages using the left menu. The most advanced version would preserve additional state within each page and each tab, such as scrollbar position, textfield input, etc, as @esDotDev has suggested. What do you think?
@chunhtai Your thoughts on whether or not the Restoration API can help would be helpful here. |
I think this's a good approach. |
This is a great thread sorry to join late. I see the topic of navigation as five topics. Theory of navigation
EDIT
Anything else I missed? Hierarchical Routing
Router View / Router Outlets
Nested Stacks
VisionIdeally we would like to have a simple but powerful navigation framework that can answer all these 5 questions. In the web, the idea of a traditional URL is that it can answer (1) (2) (5) simultaneously, leaving only (4) for the browser/client and (3) is not applicable. This is why URL is so powerful and almost complete. One could use 'data' portion of the URL to store the missing parts (3) and (4) - in which would be a very long URL. But we don't need that. Probably existing classes already exist and satisfy but if not, a new set of classes could be created to answer these questions. class NavEntry{ // this is equivalent to a URL, and could be written in the URL form
Widget view;
dynamic data;
Widget parent;
NavEntry.fromURL(String URL) {...}
}
class Nav{ // this is the overall nav
List<NavEntry> entries; // ordered
List<NavEntry> history; // ordered
List<NavEntry> stack; // this achieves the same as NavEntry.parent
Nav.hierarchical(List<NavEntry> entries) {...} //by default, creates the stack, history, and hierarchy based on the URL hierarchy
}
With that I can:
The only missing part is state perhaps is state-aware screens. Not sure if this is really a goal here. Nested navWhether "a" screen is really one screen, or multiple screens has no visual implications, and I believe that this is a philosophical discussion. Consider bottomnav for instance, which could be a Now, in terms of navigation history, stack and hierarchy, one can decide to control them separately or together. If controlled together, then just need one Navigator. If controlled separately, two or more navigators are needed in which the top most screen should be visible. I believe that if we dissect the problem this way we have a better easier understanding of the problem. |
I think that thinking of things in web terms, and using a folder/page paradigm is spot on, because we are forced to do so on the web anyways. I'm not sure I agree that it makes sense to think of things in terms of entries vs stack... I'm not sure going "Up" is worth all the problems it inherently comes with, when a simple Just as a code example, I've gotten my "path_stack" to a pretty nice place. It handles all nesting scenarios I can imagine, can easily support history like the web, or similar patterns like This creates a fairly complex nested route structure, with dedicated nested menus, persistent pages, and full-screen (non-persistent) routes. This essentially emulates the Twitter for Android app, and I could build the entire app with this approach:
I think this demonstrates a really nice mix of declarative structure, with imperative control and it's super flexible. "Back" in this example is just walking a P8X6oWc0Mh.mp4 |
@idkq I really like some of the idea your are trying to express. However there is two point where I can't agree.
Well we do need the url for Flutter web. Keep it as a second though and you will only get your API more completed when you will have to take it into account. Actually this is shown in your example,
I desagree, take the example of a |
Sorry.. this:
refers to:
...In the sense we don't need history embedded in the URL. But agree, we need URL.
We can think of animation as a stream of frames. In that sense, each frame is visually/literally one screen painted each time. |
Going up is very handy and simple. There is always one parent up to the root. If you pass a bunch of urls the app could easily built the navigation itself by inferring the paths. Going up is definitely not the ultimate solution, but should cover the most basic scenarios. The complexities lies on how we implement it. It is nothing else but a |
Right, I didn't mean we should lose "up" as a navigational concept, it just complicates things a lot at the routing level to define this declaratively. Either the page can just know it's parent (as html has always done), or can sniff it out imperatively from the history stack, seems to be a better approach to me. As you say, many different ways to theoretically inject a parent link into a child. |
fwiw, I have released my
return NavStack(
stackBuilder: (context, controller) => PathStack(
scaffoldBuilder: (_, stack) => _TabScaffold(["/home", "/profile"], child: stack),
routes: {
["/home"]: HomeScreen().buildStackRoute(),
["/profile"]: ProfileScreen().buildStackRoute(),
}));
...
// Change path using a simple api:
void handleHomeTabPressed() => NavStack.of(context).path = "/home";
void handleProfileTabPressed() => NavStack.of(context).path = "/profile";
A more detailed example, with query args, and route guards, looks like: bool isConnected = false; // change this to allow access
return NavStack(
stackBuilder: (context, controller) {
return PathStack(
scaffoldBuilder: (_, stack) => _MyScaffold(stack),
routes: {
["/login", "/"]: LoginScreen().buildStackRoute(),
["/in/"]: PathStack(
routes: {
["profile/:profileId"]: StackRouteBuilder(
builder: (_, args) => ProfileScreen(profileId: args["profileId"] ?? ""),
),
["settings"]: SettingsScreen().buildStackRoute(),
},
).buildStackRoute(onBeforeEnter: (_) {
// Redirect and show dialog warning
if (!isConnected) controller.redirect("/login", () => showAuthWarning(context));
return isConnected; // If we return false, the route will not be entered.
})
,},);},);
...
NavStack.of(context).path = "/login"; // Allowed
NavStack.of(context).path = "/in/profile/99"; // Blocked!
NavStack.of(context).path = "/in/settings"; // Blocked!
|
I think one thing that would go really far to being able to compare example would be to separate the view code, from the routing code (as much as possible). Instead of snippets, have authors prepare a complete routing solution using shared views. A set of pages, scaffolds and menus could be pre-built, and then each package author can make small tweaks to the views, but should generally leave them alone. In my own demos for example,
There's nothing unique to my router in the 300 lines of view code, other than the Currently I'm using this sort of layout to adequately tested nesting scenarios, shows a mix of full-screen and tabbed views, and stateful and non-stateful, which you often want. This seems pretty exhaustive?
|
@esDotDev It is a great idea. Number of lines is one of the most important measures. |
Sure, I took a pass on further extracting routing code from the views themselves, this led to me extracting a bunch of pre-made btns, which I assume we'll all have to customize. So main routing table is here: Opinionated btns here: Then agnostic supporting widgets are: It's still a work in progress, I wrote much of it today and yesterday, but can definitely use another set of eye on it for some feedback. Happy to roll this into it's own repo too, which we could all share via a git dep. |
I'm just going to shime in here, maybe it's not the right place though but I hope that in your future design you are going to have a clear delimitation between:
I also hope tabs are going to be handled by url. Let me touch on a few of those: OverlaysModal dialog, bottom sheet, etc are mixed in the navigation system as you have to call Navigator.of(context).pop(); Things that should not change the url should be in a separate mechanism altogether. If something is in a dialog or any sort of overlay, then it's not supposed to be shown by direct navigation. Meaning that there is no url you can refresh to directly see a specific dialog (unless the developer specifically opens it when the page is opened like an onboarding dialog). The chronological back stackThat's the essence of the router, navigating on a specific URL gets you on a page, going back goes to the page you were before. The upstackSo that little arrow at the top of an app bar that let's you go a level up in your application until you reach the root.
I personally built my nav 2.0 library by creating an upstack containing all the routes that matches a path. So the upstack for |
@cedvdb Good points I touched based on them with a different perspective here #35 (comment) Overlays -> My number: 1 - What is shown/displayed The question whether something changes the URL or not is an interesting discussion because in theory the URL text does not necessarily represent a view, but instead, the result of the URL resolution represents a view. There is a big difference between URL text and URL resolution. URL text is simple the characters on the navigation bar of the browser. URL resolution is the interpretation of the text, often using RegEx. The reason I point out this difference is that sometimes on a overlay/popup/modal the URL does change - but it does not change to cause a change in the view. Example: 'www.mysite.com' and 'www.mysite.com/?showpopup'. In this example we are passing data but we could have a new view as well 'www.mysite.com/withpopup' that would resolve to the same view but with a different Boolean variable to switch the popup. |
I totally disagree with this as things that are in an overlay should not be part of the URL. When you add sub paths to your URL there, you are implying it is a sub page, adding state management to your URL (that's what is being done here), and further more to the path is just far removed from the intended purpose of the web URL scheme. If anything you'd add it to the query parameters, but never in the path. Overlays are secondary views that are not meant to be directly accessed. They are part of the state of a page. However my point is that in all case overlays should not be part of the URL path for the same reasons that whether an input is focussed or not, whether a tooltip is shown or not, whether an animation is starting or not should not be part of the URL path. It's just the state of a page. |
I am working on flutter/flutter#80546 which will allow developer to store state that you want browser navigation to preserve and don't want it to be part of the url. |
Again, you are getting confused with URL text and URL resolving. Past the domain text, one can write anything he wants as URL text. You might not agree with the structure and best practices definitely exist but there is nothing that prevents it. If Flutter is compatible with web, then, it has to deal with it. |
Alternatively the framework could not deal with it at all and let the user do goTo(parent) when one clicks the arrow. The benefit being that the api would be more web like. The arrow is just a link.
I don't know what we are discussing about tbh. Yes anything can be written in the path and it will resolve the path to a file (for an hosted website). You can write popup in your path, still the server has to return you a file, which then opens the popup. I don't know where / if we are disagreeing on something. My point was more along the line of that
|
The Nested Routing scenario does not specify how it should be implemented. It can be implemented using the Route API using a single parser and AppState object. Pull request #34 provides sample code for this scenario, but it's not clear this solution meets our users' needs. Each of these sections is something I think should be considered as we learn more about what users are looking for.
Please comment if you feel these scenarios are important / unimportant, or if there are any misunderstandings.
Hierarchical Routing
Scenario: A developer would like to define what UI should be displayed for a route path in a hierarchical structure.
Consider an app that handles the following routes:
/
-> redirects to/home
/home
- displays the home screen with links to/home/articles
and/home/users
/home/users
/home/articles
/settings
Another scenario where this could come up is if multiple teams are working on independent parts of an app, perhaps as separate packages or libraries:
Router View / Router Outlets
Scenario: a developer would like to define a position in UI (e.g. the widget tree) where the sub-routes should be displayed.
In other frameworks this is called Nested Routes or Nesting.
Each "layer" of the tree corresponds to a part of the UI (e.g. a Widget), which defines a position in its sub-tree where the UI for the sub-routes should be displayed. In Vue, this is the
<router-view>
component, in Angular / AngularDart this is called the<router-outlet>
and in react-router this is a<Switch>
.Nested Stacks
Scenario: A developer would like display a stack of pages in a sub-section of their app's UI.
For example, Getting to the Bottom of Navigation in Flutter describes a scenario where a Navigator is required for each destination view associated with a BottomNavigationBar. This example shows a BottomNavigationBar with an inner Navigator managing a stack of screens (
/
,/list
and/text
). Even though the inner Navigator is keeping a stack of pages, the bottom navigation bar remains on-screen.The text was updated successfully, but these errors were encountered: