Closed johnpryan closed 2 years ago
cc: @jackkim9 @chunhtai
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 router-outlet
component to define where the UI for the sub-routes should be displayed:
<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 vrouter
package uses an InheritedWidget to determine what widget should be displayed in a certain sub-section of the widget tree:
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:
I think this is interesting, not as a new feature, but really something which makes development easier.
What you describe here can be achieved in every packages as long as there is a path
-> widget
mapping. But what is really interesting is how the package makes it easier for developer by allowing or not things like relative paths.
Here is an example (using vrouter
since I think the syntax is easy to understand in that case):
Solution 1, any path
-> widget
mapping would allow that
VRouter(
routes: [
VWidget(
path: '/home',
widget: HomeScreen(),
stackedRoutes: [
VWidget(path: '/home/articles', widget: ArticlesScreen()),
VWidget(path: '/home/users', widget: UsersScreen()),
],
),
],
)
Solution 2, in vrouter
this is relative path, but the important thing is that in avoids verbosity
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 ArticlesScreen
and UsersScreen
and then added those later in HomeScreen
, they will have to go to each push
and change from push('/articles')
to push('/home/articles')
and push('/users')
to push('/home/users')
. So painful and hard to maintain separately.
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.
First, I don't really understand the difference between the two. If any, I would say that Router View / Router Outlets
is a subpart of Nested Stacks
.
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.
To do this in Navi
, nested RouteStack
doesn't need to know the URL of the parent stack.
For example, the package will automatically know how to merge the current URL in parent stack (ex. /books/1
) and nested stack (ex. /reviews
) to generate the final URL for you (ex. /books/1/reviews
).
btw, @lulupointu, Large Scale Apps is an useful link.
@lulupointu From what I learn from your reply (please correct me if I am wrong), this topic can be separate into two different scenarios.
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!
Multiple teams that works independently on different parts of the same page.
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 vrouter
, which will illustrate the points which are awaited when we talk about modularity:
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:
push('/network')
in settingsRouter
, this should not be changed when settingsRouter
is used inside mainRouter
(even if the path when using mainRouter is /settings/network
)/login
and /settings/network
would be done using something like a root router. This link will be dead when using loginRouter
independently of course.Something interesting to note here is that using this implementation provides no breaking changes to the current vrouter
. Which answer your concern:
Adding support for such modularity in the routing system is unlikely to be free of a usability cost
I don't think this has to be true. This example is using vrouter
because I am familiar with it but the important thing is that it is theoretically possible.
@InMatrix In Navi
, a nested route is just a widget RouteStack
and works like a normal widget. RouteStack
s work independently and don't need to know each other. Navi
will combine the URL parts provided by them and merge into a single URL automatically.
It means that, using Navi
, it's freely to distribute the work exactly as we are doing with normal flutter project with the normal widget tree.
In case, the child RouteStack
want to access parent RouteStack
to read the current state or to navigate, the teams only need to know what is the StackMarker
for the stack. StackMarker
is just an immutable data class, you can optionally provide when you create RouteStack
.
So, at the end, Navi
is friendly to the large project and support both issues we mentioned above by default without doing anything.
Hi folks, I've added some scenarios for my project here: https://github.com/aliyazdi75/gallery/discussions/5#discussioncomment-571684 Hope these are useful and tell me if they are wrong.
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: BooksPage(bookId: ...)
, at which pt teams can work and test fairly independently as long as they can set the initial route locally. I guess if there are complex sub-routes I can see the argument of why having your own nested router would be preferable.
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:
/home
, /explore
, /messages
etc)/messages/[MessageID]
/search?q="..."&src="..."
/explore/trending
, explore/news
, explore/sports
settings/notifications/advanced_filters
or settings/explore/location
/someuser/[PostID]
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". 1) In it's simplest form, nested routers is just an extension of map:widget mapping, where you can construct relative paths:
Route(
path: "settings/",
child: SettingsPage(),
nested: [
Route(path: "alerts", child: AlertsPage()) // Matches settings/alerts
]
)
2) In a more advanced form, nested routes allow you to wrap a scaffold around the contents of each route. This is very important for having tab menus, and nesting them (as you see in the twitter android app). There is usually be some way to define custom animations for these routes as well.
Route(
path: "settings",
nestedBuilder: (_, c) => SettingsScaffold(c), // All nested routes are wrapped in this
nested: [
Route(path: "alerts", child: AlertsPage()),
Route(path: "profile", child: ProfilePage()),
]
)
3) In an even more advanced form, each nested page actually maintains state. You can see this in the twitter android app. When you change tabs, scroll-position and sub-tab values are maintained, there is no loading or loss of state when you switch routes. Additionally, the state of the Scaffold widgets are also preserved, so you can animate your tab menu when changing routes.
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:
https://user-images.githubusercontent.com/736973/114346715-23190380-9b21-11eb-85d9-35dd7be45126.mp4
This is constructed declaratively, from code looking like:
return PathStack(
path: currentPath,
childBuilder: (_, stack) => MainScaffold(child: stack),
transitionBuilder: (_, stack, animation) => FadeTransition(opacity: animation, child: stack),
entries: [
/// Home
PathStackEntry(path: "home", builder: (_) => HomePage("")),
/// Settings
PathStackEntry(
path: "settings/",
builder: (_) {
return PathStack(
path: currentPath,
parentPath: "settings/",
childBuilder: (_, stack) => SettingsScaffold(child: stack),
entries: [
PathStackEntry(path: "profile", builder: (_) => ProfileSettings("${Random().nextInt(999)}")),
PathStackEntry(path: "alerts", builder: (_) => AlertSettings("${Random().nextInt(999)}")),
PathStackEntry(path: "billing", builder: (_) => BillingSettings("${Random().nextInt(999)}")),
],
);
},
),
],
);
It's all driven by a single ValueNotifier<String> urlValue
right now, but shouldn't be too hard to tie into Router instead. Under the hood, PathedStack
is basically just an IndexedStack
+ Map<String, Widget>
https://gist.github.com/esDotDev/09b0cb9fe2604c44b1d5a642d5a9ac29
My plan right now with this is to release a path_stack
package, that would have nothing to do with Navigation per se. Then build a very small nav_stack
package that just wires up path_stack => MaterialApp.Router. Will also add some mixins or builders to support route guarding and param parsing. I think this should be pretty easy, as I plan to follow the web-paradigm of just-in-time redirects and parsing within the pages themselves.
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.
https://user-images.githubusercontent.com/736973/114435199-2217be80-9b81-11eb-872d-f885ad87b161.mp4
The back button here looks like:
onPressed: () {
String prevPath = PathStack.of(context).history.last;
urlNotifier.value = prevPath; // todo: Change the router url instead
},
Regular links look like:
urlNotifier.value = "${AppNav.tabsCategory}${SettingsHome.path}${ProfileSettings.path}");
I think when I add a NavStack wrapper around PathStack, we can provide convenience methods like NavStack.of(context).goBack()
to make these even easier.
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:
return PathStack(
path: currentPath,
key: pathStackKey,
entries: [
PathStackEntry(
path: AppNav.tabsCategory,
builder: (_) {
/// Main-app path-stack, wraps all childed in MainScaffold
return PathStack(
path: currentPath,
parentPath: AppNav.tabsCategory,
// Main scaffold is wrapped here
childBuilder: (_, stack) => MainScaffold(child: stack),
transitionBuilder: (_, stack, animation) => FadeTransition(opacity: animation, child: stack),
entries: [
/// Home
PathStackEntry(path: HomePage.path, builder: (_) => HomePage("")),
/// Settings
PathStackEntry(
path: SettingsHome.path,
builder: (_) {
/// Settings PathStack, wraps all Settings children in SettingsScaffold
return PathStack(
path: currentPath,
parentPath: AppNav.tabsCategory + SettingsHome.path,
// Settings scaffold is wrapped here
childBuilder: (_, child) => SettingsScaffold(child: child),
entries: [
PathStackEntry(path: ProfileSettings.path, builder: (_) => ProfileSettings("")),
PathStackEntry(path: AlertSettings.path, builder: (_) => AlertSettings("")),
PathStackEntry(path: BillingSettings.path, builder: (_) => BillingSettings("")),
],
);
},
),
],
);
}),
/// Full-screen Routes, note that maintainState is deliberately turned off here, so we get a fresh compose sheet each time
PathStackEntry(path: ComposePage.path, builder: (_) => ComposePage(""), maintainState: false)
],
);
@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?
@InMatrix
We could have multiple nested routes scenarios for isolated problems. Then we could introduce a more complex nested routes scenario to combine them. What do you think?
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:
https://user-images.githubusercontent.com/736973/114454857-49c65100-9b98-11eb-965a-e1ef6010ab37.mp4
The first 3 are handled automatically if the StatefulElement
of each route is truly preserved in memory. Otherwise, it's a lot of work for devs to support. I think a truly great routing solution for Flutter would just handle this for us.
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. https://github.com/flutter/flutter/issues/80303
We could have multiple nested routes scenarios for isolated problems. Then we could introduce a more complex nested routes scenario to combine them.
@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?
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...
@chunhtai Your thoughts on whether or not the Restoration API can help would be helpful here.
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
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.
What should be shown in screen? This seems simple but it is rather complex. For static screens it is basically a snapshot of visual elements. For animations, we can think of a stream of snapshots. In abstract terms a uid/key/URL should identify a screen to be shown. This is what the URL does for web.
What data should be passed and kept in memory?
This is the book:id
for deep link this is the data that should be passed to the screen for whatever reason. It is the metadata encoded on the uid/key/URL mentioned above. Anything can be encoded as data, since the characters themselves is data (i.e. www.exampleAAA.com
where ...AAA
is ASCII for 65 65 65)
What is underneath and above? This is a special case for stacks, transitions, etc. when one removes a top layer/card from the stack. Here order is important, since removing one or more cards should reveal the screens in order. Of course we are dealing with 1 dimension here, in which x is either on top or below each other (2 dimensions would be x and y & 3 x, y ,z).
What is the history? Self explanatory, a stack of navigation - the previous screens. One important question here is wether we should have a state-preserving history, in which we need not only the uid/key/URL but also the state, since sometimes the URL is not sufficient to recreate the state.
What is the hierarchy? This is different form (3) and (4) because it is hypothetical. Take the example of a folder structure. It has a parent and child. The user can navigate directly to the child without going to the parent, so the history (4) is independent of the structure. The user can then go up one level, which reveals the parent. This is different from a stack (3) because one can have the same screen reference in multiple hierarchies (which does not happen on a folder structure).
Anything else I missed?
Ideally 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.
Whether "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 Navigator
inside another, or two stacks of cards inside one another (if it was possible). Would you say, in a flat surface that it is two screens or one screen? It does not matter.
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 back()
or goto(otherPage)
will often do. The web has done without this paradigm for decades, they just embed links to specific "parent" pages to support this.
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 popUntil
to popMatching
.
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:
return PathStack(
path: currentPath, // eg. settings/alerts or details/99
key: pathStackKey,
routes: {
[AppPaths.tabsCategory]: StackRouteBuilder(builder: (_, __) {
/// Main-app path-stack, wraps all childed in MainScaffold
return PathStack(
path: currentPath,
basePath: AppPaths.tabsCategory,
// Main scaffold is wrapped here
scaffoldBuilder: (_, stack) => MainScaffold(child: stack),
transitionBuilder: (_, stack, animation) => FadeTransition(opacity: animation, child: stack),
routes: {
// Home
[HomePage.path]: StackRouteBuilder(builder: (_, __) => HomePage("")),
// Settings
[SettingsHome.path]: StackRouteBuilder(
builder: (_, __) {
return PathStack(
path: currentPath,
basePath: AppPaths.tabsCategory + SettingsHome.path,
// Settings scaffold is wrapped here
scaffoldBuilder: (_, child) => SettingsScaffold(child: child),
routes: {
// Use a "" alias here, so links to `tabs/settings/` will fall through to this view
[ProfileSettings.path, ""]: ProfileSettings("").buildStackRoute(),
[AlertSettings.path]: AlertSettings("").buildStackRoute(),
[BillingSettings.path]: BillingSettings("").buildStackRoute(maintainState: false),
},
);
},
),
},
);
}),
/// Full-screen Routes
[ComposePage.path]: ComposePage().buildStackRoute(maintainState: false),
// Supports reg-exp based matching on path suffix
[DetailsPage.path + ":id"]: StackRouteBuilder(
maintainState: false,
builder: (_, args) => DetailsPage(itemId: args["id"]),
)
},
);
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 List<String> history
, "Close" is just going back in history before it sees a page that is not Details, and it jumps there.
https://user-images.githubusercontent.com/736973/114611233-0d5d2880-9c5e-11eb-892e-5db3a90f64f6.mp4
@idkq I really like some of the idea your are trying to express. However there is two point where I can't agree.
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.
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, NavEntry.fromURL(String URL) {...}
. The url has to store the information anyway since you convert it to your NavEntry
here.
Whether "a" screen is really one screen, or multiple screens has no visual implications, and I believe that this is a philosophical discussion.
I desagree, take the example of a Scaffold
with an animated BottomNavigationBar
such as this one. If when navigating you only change the body
of the Scaffold
, the BottomNavigationBar
will be animated nicely. If you change the entire screen however, this won't work.
Sorry.. this:
But we don't need that.
refers to:
One could use 'data' portion of the URL to store the missing parts (3) and (4) - in which would be a very long URL.
...In the sense we don't need history embedded in the URL. But agree, we need URL.
I desagree, take the example of a Scaffold with an animated BottomNavigationBar such as this one.
We can think of animation as a stream of frames. In that sense, each frame is visually/literally one screen painted each time.
I'm not sure going "Up" is worth all the problems it inherently comes with
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 goto
parent.
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
goto
parent.
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 nav_stack
and path_stack
packages:
https://pub.dev/packages/nav_stack
https://pub.dev/packages/path_stack
NavStack
allows you to express a full routed, tabbed scaffold, with persistent state like:
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 onPressed
handlers.
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?
/tabs
/home
/settings
/profile
/alerts
/billing
/messages
/friends
/unread
/archived
/compose (maintainState: false)
/details (maintainState: false)
@esDotDev this seems like a great exhaustive example. Can you share the code? I would like to test this UI with beamer. I definitely agree with your goals of focusing on routing only and keeping the rest minimally changed. I suggested the same here at the beginnings of this UXR :+1:
@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: https://github.com/gskinnerTeam/flutter_nav_stack/blob/master/example/lib/advanced_tabs_demo.dart
Opinionated btns here: https://github.com/gskinnerTeam/flutter_nav_stack/blob/master/example/lib/advanced_tabs_buttons.dart
Then agnostic supporting widgets are: https://github.com/gskinnerTeam/flutter_nav_stack/blob/master/example/lib/advanced_tabs_demo_pages.dart https://github.com/gskinnerTeam/flutter_nav_stack/blob/master/example/lib/advanced_tabs_demo_scaffold.dart
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:
The back stack (I personally call it the upstack)
I also hope tabs are going to be handled by url.
Let me touch on a few of those:
Modal 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).
That'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.
So 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. For this I'll go against the grain, but unless the up stack as a performance benefit I don't think it's a good architecture to rely on. It would be much easier to either:
or let the user specify it altogether, at that point it is just a link.
I personally built my nav 2.0 library by creating an upstack containing all the routes that matches a path. So the upstack for /products/:id
contains 3 elements (ProductDetails, ProductList, Home). However I don't see any benefit on having 3 elements in the stack instead of one and I think ditching that concept would make things easier.
@cedvdb Good points I touched based on them with a different perspective here https://github.com/flutter/uxr/issues/35#issuecomment-818974846
Overlays -> My number: 1 - What is shown/displayed Chronological back stack -> My number: 4 - History The upstack -> My number 5: - The hierarchy
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.
www.mysite.com/withpopup
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 https://github.com/flutter/flutter/issues/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.
things that are in an overlay should not 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.
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. (...) It is nothing else but a goto parent.
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.
Past the domain text, one can write anything he wants as URL text.
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 window.history.back()
will always bring you to the last page visited while Navigator.pop()
might just pop an overlay. Yes they are not the same thing but I'm advocating for a more stateless web like approach. Put the overlays in their separate package to not conflate them with navigation. IE:
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 https://github.com/flutter/uxr/pull/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.