Closed InMatrix closed 3 years ago
I'd like to clarify the intent and scope of the project, prompted by some questions in #4.
First, the primary goal of this project is to design or endorse an easy-to-use package for implementing common navigation patterns through usability research. While @johnpryan has prototyped the page_router package, we're not attached to a first-party solution. At the moment, it's mostly used as a tool for investigating where the sweet spot might be between API usability and flexibility. We are aware of several community efforts around simplifying Navigator 2.0, and a potential outcome of this project is to recommend one of them in a way similar to how the provider package became the recommended state management solution for relatively simple Flutter apps.
But we need to establish a standard to make sure whatever we recommend or choose to build ourselves meets the needs of Flutter users and strikes the right balance between usability and flexibility. This project is about establishing that standard and developing a method for measuring whether a package has reached that standard. The first step, which is discussed in this issue, is to define the set of navigation scenarios most Flutter developers would need and design the high-level API for these scenarios. The biggest question we're looking into right now is whether nested routing should be supported by a high-level navigation API or not, and how much supporting that could negatively impact the usability of the API in other scenarios such as deeplinking.
The scenario definition work drives the next few activities in our research and design process. We'd like to include a community package in our API usability studies alongside page_router
, so we can understand how users react to different API design approaches. The results of those studies will be published in the wiki section of this repo. If you're an author of a navigation package, we'd love to get in touch and learn about your future plans.
Last, we recognize that usability was insufficiently considered in the design process of Navigator 2.0 and we'd like to develop a process for not only facilitating the simplification of Navigator 2.0 through high-level APIs but also aid future API design projects in Flutter.
Thank you for opening this discussion. I'm the creator of beamer and would like to contribute to this investigation.
Beamer was created during my development of a fairly large mobile/web app, trying to abstract the concepts from this article by John Ryan and an official design doc.
I ended up forming an idea of BeamLocation
which represents the RouterDelegate.currentConfiguration
. It holds everything related to path
and is responsible for providing pages
to the Navigator
. The pages
list is dynamically created, conforming to the rules the developer has set having examined the available path
properties. Common usage is that the developer groups "related" Page
s into a custom implementation of BeamLocation
to which the user can then beamTo
and the appropriate stack will be built. User can also update it's current stack via updateCurrentLocation
.
The end result is having all the rules for app's routing in a single place and developer needs not to worry much about "navigation state". It ends up being similar to named routes, but now we have "named locations". There are also some additional features such as beamBack
and guards.
The package is still in early stages and with lots of improvements planned, all of which can be monitored at issue board. I have made examples of some scenarios and they are available as gifs in README and as full code samples at example (the original books example) and examples.
Any contribution is welcome and I believe this discussion will be of great help for future work.
Hi,
First of all, thanks flutter team so much for listening to developers and making the next big change to Navigator API.
I am an Angular developer with some years of experience. Even I still have some issues with angular router in my projects, I love angular router a lot. With the experience when working with angular router, I checked all the navigation packages I found on pub.dev, but to be honest, I don't like all of them. I will definitely not use them for my projects. Therefore, last month, I started to create my own project to copy angular router design to flutter. I do not publish it yet, and will not publish it because now I believe flutter team will soon deliver a better official API.
So, I would like to give my feedback from what I learned when I tried to copy angular router design to flutter.
From my point of view, flutter could copy almost stuffs from angular router with improvements for usability and flexibility. Below are some improvements I am trying to do while copying angular router to flutter:
Instead of Route Guard, I would like to have a more general solution, for example, Route Interceptor. Each route should accepts multiple route interceptors, running in a chain with configured ordering. Child-routes (or sub-routes) should inherits all route interceptors from parent routes. The interceptor could be used for checking if user is allowed to route from route A to route B. If not, developers can choose what to do, ex.
Not only that, interceptor is a good place to create/destroy required instances for the route or integrate with an injection solution. For example, I could use GetIt to create/destroy an injection scope for any specific route and its sub-routes.
/
, /products
, /appoinments
. When we integrate with main app, the main app will configure to have /customers/:id
as parent route of module A. So the final routes in our app becomes: /customers/:id
, /customers/:id/products
, /customers/:id/appoinments
when we integrate module A to the main app.StreamBuilder
widget to provide new route state and with this, the app navigate to next route automatically when route state is changed. The state should contain the information about current route, the history stack (NOT the route hierarchy stack in the current implementation of Navigator 2.0). It would be nice if flutter API can provide these information, so we only need to reuse them for our bloc/stream and reduce a lot of work to manually manage route state.Router.to('/a/b/c')
Router.back()
Router.up()
Router(
initialRoute: '/public', // default should be `/`
route: Route(
'',
interceptors: [AuthInjectionInterceptor(), HomeInterceptor()],
children: [
Route(
'auth',
builder: () => AuthPageWidget()
),
Route(
'public',
builder: () => PublicPageWidget(),
interceptors: [PublicInjectionInterceptor()]
),
Route(
'private',
interceptors: [AuthInterceptor(), PrivateInjectionInterceptor()],
children: [
Route('', redirectTo: 'a'),
Route('a', redirectTo: 'b', relative: true), // `a/b` does not match this route!
Route('b/**', redirectTo: '/private/c', relative: false), // `b/c` matches this route!
Route('c', builder: () => PrivateCWidget()),
Route('d?/**/e*', builder: () => PrivateDEWidget()), // a complex ant path is useful here
Route('**', builder: () => LastPrivateWidget()), // `/private/f/g/h` does not match all previous paths, show LastPrivateWidget()
],
),
Route(
'**',
builder: () => const Center(
child: Text('404 - Page Not Found!'),
),
)
],
)
)
class AuthInjectionInterceptor implements RouteInterceptor {
// Normally, we should use an injection framework here. For example, I could choose to use GetIt with scopes.
// before enter the route, inject AuthService instance
// before exit the route (because we use this interceptor for the root route, it means exit app), destroy AuthService instance
}
class HomeInterceptor implements RouteInterceptor {
// because, AuthInjectionInterceptor injected the AuthService in the root route, we can get the instance here.
// if unauthenticated => redirect to '/public', stop next interceptors
// if authenticated => redirect to '/private', stop next interceptors
}
class AuthInterceptor implements RouteInterceptor {
// because, AuthInjectionInterceptor injected the AuthService in the root route, we can get the instance here.
// if unauthenticated => redirect to '/auth', stop next interceptors
// if authenticated => continue to run next interceptors => if passing all interceptors, allow to navigate to the desired route
}
class PrivateInjectionInterceptor implements RouteInterceptor {
// Normally, we should use an injection framework here. For example, I could choose to use GetIt with scopes.
// before enter the route, inject multiple required instances for private pages
// before exit the route, destroy multiple required instances for private pages
}
Hi, many thanks for this, excited to make this better. Few general comments:
Regarding standards, in my opinion:
Hope it helps as a start.
Hi,
I am the developer of the vrouter plugin. My approach was based on Vue.js router.
The basic idea is to make a routing tree at the root of the application, describing your paths and the widgets which belong to each paths. I believe this is working very well in terms of simplicity and flexibility.
Here is a basic example:
VRouter(
routes: [
VStacked(
path: '/login',
widget: LoginWidget(),
),
VStacked(
path: '/profile',
widget: ProfileWidget(),
subroutes: [
VStacked(
path: 'settings', // This matches the path '/profile/settings'
widget: ProfileWidget(),
),
],
),
VRouteRedirector(
path: ':_(.*)', // This matches any path
redirectTo: '/login',
)
],
)
As you can see, this makes for something very condensed. However there are a lot of other functionalities such as:
I created a website to explain how to use it, in hope to make it easy for anyone to use: vrouter.dev
Many of us use flutter_bloc
for state management. It'd be great if the docs include an example of how to properly use Navigator 2.0 with cubits/blocs.
Another good example of a very common scenario is Firebase Analytics integration. The default FirebaseAnalyticsObserver
has a lot of limitations and can't be used in a real-world app.
It is also unclear how to present various modal pickers. What is the right way of doing this?
I created the 6 scenarios describes in Navigation scenario storyboards using vrouter.
I tried to produce minimal yet realistic implementation of the described situations. Please tell me if you want me to comment the code more than I did, or if I misunderstood any of the situations.
If you are tired of copy/pasting, you can also clone this repo
I hope to hear want you think !
@lulupointu That's awesome. Thank you so much. I played with vrouter yesterday and I found it really easy to get started. The way it handles nested routes feels easy to understand as well. The scenario implementations you shared will greatly help us conduct the comparative analysis we plan on doing next (#7).
Hello! flutter_modular use Navigator 2.0 as well. https://pub.dev/packages/flutter_modular/versions/3.0.0-nullsafety.45
Another idea is to use the way of Spring Framework MVC to handle routes with annotation. In the controller method, developer can decide to build a widget or redirect to another route.
The main advantage is that, path and query parameters could be handled immediately and passing to widgets safely. So widgets don't need to care how to handle these data from the current route. But I think with other approaches, we can also find a solution for this problem easily.
The one thing that seems missing from the proposal, and honestly you see this quite often in the Widget library itself, is lack of research into prior art. When you have things like React Router, in it's 4th iteration, after years and years and thousands of real world apps, we should borrow from their insight!
Looking at the latest implementation of React Router (v4) 2 things stand out immediately:
In terms of dev experience, this is vastly more focused, and easier to understand: https://reactrouter.com/
If flutter can get anywhere close to this ease of use, I'm sure developers will be extremely pleased.
I think the approach of react router is the most similar to the way of flutter widget. It worth to check it. IMO, react router is much more powerful than the way of angular and vue.
@InMatrix We just triaged a proposal https://github.com/flutter/flutter/issues/77360, we like to know if it possible or could be implemented in the future Thanks!
@TahaTesser
It's definitely possible and in fact quite natural in Navigator 2.0, as the developer has control over Navigator's entire stack of pages.
@TahaTesser This is a good example of the Skipping Stacks (#5) use case. We probably should consider incorporating that in our scenario deck. Cc: @jackkim9
Hey everyone!
I'm the author of Yeet. I'm focusing on 1. Keeping things extremely simple for first time developers (something that just works!) and 2. Treating web as a first class citizen.
I'm currently teaching a 3 week course on flutter and spending 2 days to really teach something like navigator 2.0 is not a viable option for beginners.
My solution was to create something that feels like yet another tree (we're using flutter after all). I use a simple depth first search to find the first match and populate the page stack using that. It's still quite experimental but the main functionalities do work.
There are use-cases that I think are not covered by the current examples:
Windows resizing / responsive design. Web/Desktop application, due to the platform relying on windows instead of full-screen pages, may want to handle resizing the window to use a different layout. The difficulty is in preserving the navigation and application state during layout switch.
Next-route pre-loading For performance, applications may want to lazy-load data or widgets (such as with the new deferred components) The side-effect is that when navigating to a new route, these objects may not be loaded yet, which lead to showing progress indicators. To improve the user experience, a common practice is trying to preload the "next" route. An example would be:
Hyperlink accessibility A link in the browser should be compatible with screen-readers and browser tools (such as right click -> open in new tab). In an accessible app, state-first approaches would end-up with:
Link(
url: '/books/1', // needed for screen readers, but otherwise does nothing
onPressed: () => setState(() => selectedBookId = 1), // handle the redirection
)
whereas url-based approaches could simplify it into:
Link(
url: '/books/1', // both used by screen readers and for navigation, as it hooks into `Navigator.pushNamed`
)
state restoration How much work is required to support restoring the navigation history if the app was killed by the system? Does it supports anonymous routes? (Do we even want to support them?) URL-first approaches like Nav v1 are really good at this, since they can serialize the String directly.
@rrousselGit These are great considerations. Some of them came up in earlier discussions, especially with the regard to the interplay between routing and state management. We decided to keep it simple in this evaluation, but routing API designers should take the issues you raised into consideration. I'd love to see responses to your questions from package authors.
Of these 4 issues, 1 of them is outside the hands of package authors, which is preloading an off-stage widget or routes. Flutter needs better support in general for keeping things alive, or pre-cached when off stage. Short of using IndexedStack
, it's cumbersome to keep anything alive in Flutter once it goes off screen, (or before) which is a mobile-first optimization that really has no place on Desktop.
The other 3 issues all point to why state-first navigation is a really bad idea. When you have 2 sources of truth (url path OR state), it gets very messy very fast. Because web urls are a fact of life, we can not get away from them, which means you either choose URL-first approach, or you have to deal with the huge headache that is reconciling state and url.
It becomes clear very quickly in production, that state needs to go, and URL-first is the only sane path forward.
It reminds me of 10yrs ago, before unidirectional app state caught on. It was never exactly clear who was changing the state. Eventually everyone got on page that this was a big headache and mess, and agreed that state-changes should be top down, with the views binding to the models but not changing them directly.
I think it would be a good idea at some point, for the Flutter team to take a step back, understand that state-first nav is actually not a good approach for the majority of apps, and maybe engineer a URL-first approach that is more robust and flexible than Nav 1.
Basically if we have to be on web and have URL's (which we do) then just follow the imperative web-like navigation pattern that is well understood and robust.
Of these 4 issues, 1 of them is outside the hands of package authors, which is preloading an off-stage widget or routes. Flutter needs better support in general for keeping things alive, or pre-cached when off stage.
I disagree
For example with state-first approach, we could potentially support:
SomeNavigator(
routes: [
if (page != null) ...[
MyRoute(path: '/book/$selectedBookId/page/$page'),
Offstage(child: MyRoute(path: '/book/$selectedBookId/page/${page + 1}')),
],
]
)
I would also not call url-first as "imperative"
For me the imperative APIs in Navigator 1 are not "pushNamed" functions but rather:
push(context, Route)
, for similar reasons. Once pushed, the route cannot be updated.This is in opposition to things like MaterialApp.routes
/MaterialApp.home
, where you can rebuild the MaterialApp
widget with different routes/home
Of these 4 issues, 1 of them is outside the hands of package authors, which is preloading an off-stage widget or routes. Flutter needs better support in general for keeping things alive, or pre-cached when off stage.
I disagree
For example with state-first approach, we could potentially support:
SomeNavigator( routes: [ if (page != null) ...[ MyRoute(path: '/book/$selectedBookId/page/$page'), Offstage(child: MyRoute(path: '/book/$selectedBookId/page/${page + 1}')), ], ] )
Sure, but this is not very scalable and will quickly turn into a mess. It's much simpler and more flexible to just give MyRoute someway it can cache the next route, and then show that cached route when it needs. But flutter doesn't support this as everything needs to be 'on-stage' before it can be initialized. In AIR, or Unity, I could simply construct my new page, and it would begin loading data, I could then toss it on stage whenever I want.
Though, to be fair, probably more effective anyways to preload the data that the view needs, and then when the view loads in, there is nothing to fetch.
model.loadPageData(page + 1)
For me the imperative APIs in Navigator 1 are not "pushNamed" functions but rather:
I think you're getting far away from the classic definition of imperative here but this is semantics. I just mean that routes are explicitly navigated to via a function (like pushNamed
), rather than implicitly shown based on whatever the current state of the application is, which does not scale well at all, and provides no solution for routes that are not tied to state, you end up with cruft like appModel.isSettingsPageOpen
and inevitably clash as you add more and more of these flags.
The other 3 issues all point to why state-first navigation is a really bad idea.
I agree but I would say that much more cons to state navigation exist than reconciling state and URL. By state navigation I mean having several variables controlling which screen should be rendered and be presented as the active screen. The variables are inter-related and inter-dependent. For example if isLoggedIn is false present login screen, if searchingProduct is not null present Search screen. But if isLoggedIn is true and searchingProduct is not null, present Home Screen.
Is this easy to maintain? No.
State navigation is preposterous in 90% of the cases. State navigation is a conditional stacked navigation, that is, the navigation and screen is rebuild based on a series of 'if' conditions.
How many variables do you need to determine which screen is active? Only one. Not one per screen. One can argue that the existing state variables used for other purposes are being re utilized for navigation but, no, that is not a benefit.
Let's compare it to an useful state utilization. Say you have a Switch that filters some results. The state of the switch can modify many aspects of that screen including the color of the switch itself. The switch state is binary but the widget tree can change significantly based on the state.
Going back to navigation the process is similar but useless. The change of navigation state most of the times occur when an event is triggered (press button etc). Like the switch, the screen is either presented or not (other screen in the stack would be presented). The problem here is that absolutely nothing depends on the state of the navigation in 90% of the cases. The entire screen is assumed to be active for any widgets to be shown. The navigation state is never consumed for any other means. It is a waste of time for over engineering it.
So in summary, state navigation uses many more variables than necessary, makes the code hard to maintain, makes state available but for no widget to consume, and requires syncing with URL.
The original snippets written for Learning Flutter's new navigation and routing system used a state-driven approach, which is probably not the best-practice for routing, as is being discussed.
There's a new sample in the flutter/samples repo, navigation_and_routing, which demonstrates a general-purpose way to use the current URL path as the app state for navigation.
@johnpryan Excellent job! I really like this way better. Loved it this complete example and implementation overall. Kudos!
Here is what I don't like (no criticism to the code whatsoever, but conceptually, in line with the previous discussion on variables and state manage):
On BookstoreNavigator
, selectedBook
and selectedAuthor
are driving the navigation stack: it dictates which screens will be stacked - this makes no sense to me. First of all, there is no 'selected' book or author concept. Instead, the user is simply 'viewing' a book page or author page. The difference is important because if we treat every (or some) page-parameters as conceptual active/inactive object, things can get messy.
Secondly, the only consumers of selectedBook
and selectedAuthor
are their respective screens, similarly to a single web-page request, this should be scoped down, and not up. Why do we need to parse and use it in the navigator? We need it only to put the MaterialPage
into the stack. So, selectedBook
and selectedAuthor
are not being used anywhere else in the app except their own screens. It means that as the app grows it would have several useless variables and ifs in this BookstoreNavigator
class for no reason. If only BookDetailsScreen
cares about param book, it should be limited to that scope and not other parts of the app.
An argument can be made that another process could change the state of that variable hence it would activate the screen. But this is not what is happening in this app and it is an unlikely use case overall. As an analogy, you are currently reading this GitHub webpage. Changing the state of a parameter of another webpage does not rebuild your navigation stack.
A better approach in my view would be the navigation stack be built based on the active RouteStateScope
path hierarchy. Think of it as a Site Map structure. For example:
BooksScreen
BooksScreen
+ BookDetailsScreen
For that to happen a linkage of some sort would need to be implemented, such as 'parent' for each page. So that when pop() is used it moves to the 'parent' route.
That could also be used on the outer Navigator
in BookstoreScaffoldBody
to replace the ifs. In case there are no parents, show the main view.
Please lmk if I'm missing something... I just can't see a use case in which bringing these variables up to the widget tree is applicable unless rare cases of monitoring, services and sensors.
@idkq I agree with your points. Have you seen how beamer works?
The scoping of navigation variables for use cases in large applications is exactly what beamer aims to solve with the concept of BeamLocation
s. The process looks something like this:
RouteInformation
comes into the appBeamerDelegate
(RouterDelegate
) takes it and chooses the appropriate BeamLocation
to which to pass this RouteInformation
. In this process, the pure URI (e.g. /books/2
) stays in BeamerDelegate
(without additional variables like "selected book" from path parameter).BeamLocation
creates its own state
based on pure RouteInformation
passed in above step. At this point, this state
can be anything what developer defines, but there is also a default BeamState
data object that will extract pathParameters
and such variables. The state
just needs to mix with RouteInformationSerializable
in order for BeamerDelegate
to understand what route to report when currentBeamLocation
updates.BeamLocation
builds a stack of pages to put into Navigator.pages
based on its state
.After this is completed and we are at /books/2
with a page stack of [BooksScreen, BookDetailsScreen]
, we can navigate
BooksBeamLocation
/ page stack by updating its state
(e.g. setting selectedBook
to null, which would effectively make a pop
) or via beamToNamed('/books')
which will have the same effect.beamToNamed('/articles/3')
(and some other ways) which will run the above process again, choosing the ArticlesBeamLocation
this time, for example.Here, the information about "selected book" or "selected article" is localized within the appropriate BeamLocation
; above just the relevant page stack, and not above everything. The same can be done for scoping certain additional Providers
and such (e.g. CartProvider
for all shop-related page stacks), but this is just an additional feature of BeamLocation
s, not related to navigation.
As for your idea of "1 path segment = 1 page", this is the default behavior of beamer, but can also be overridden if developer needs. When creating a navigation package, some default behavior needs to be picked and this is, in my opinion also, the most sensible one.
NOTE: Some of my statements above might be confusing (especially regarding the types I mention) if compared to the current (v0.14.1
) README of beamer. This is because I'm in the middle of implementing these improvements. Basically
RouteInformation
, the v0.14.1
documentation would say BeamState
RouteInformationSerializable
concept in v0.14.1
. BeamLocation
s can still have custom states in v0.14.1
, but this is not so clean nor clear as it will be in v0.15.0
@slovnicki I like that beamer builds the page stack purely on the URI info.
Do you see a valid scenario in which the page stack structure changes at runtime? If not, I believe BeamLocation
could be further simplified (perhaps a new SimpleBeamLocation
?) that does not have the ifs such as if (state.uri.pathSegments.contains('books'))
. Just an idea.
The build method of the super class would be in charge of determining which pages to put or remove (not put) in the stack.
@override
List<BeamPage> buildPages(BuildContext context, BeamState state) => [
BeamPage(
key: ValueKey('home'),
child: HomeScreen(),
path: '/'
),
BeamPage(
key: ValueKey('books'),
child: BooksScreen(),
path: '/books'
),
BeamPage(
key: ValueKey('book-${state.pathParameters['bookId']}'),
child: BookDetailsScreen(
bookId: state.pathParameters['bookId'],
),
path: '/books/:bookid'
];
The buildPages would loop trough all BeamPage
list and remove the ones not needed and keep others based on BeamPage.path match with current URI. Maybe key
could be used for path url if you don't want to create a new attribute.
One problem is when you have a bifurcation, such as '/' -> '/books' and '/' -> '/authors'. For that perhaps use a new BeamLocation
or create a new class BeamPageBifurcation
that takes a list of BeamPage
.
It is a minor thing but just for maintenance leaving the URI resolving at the backstages as opposed to be controlled by the developer.
Potentially even path
attribute could be omitted. The logic would be to match the number/count of BeamPage
to the level of the URI. For example, '/' = 1, '/xxxx' = 2, '/xxxx/yyyy' = 3 (count of forward slashes plus one if any text). Not sure for readability though.
@override
List<BeamPage> buildPages(BuildContext context, BeamState state) => [
BeamPage( //resolves from '/'
key: ValueKey('home'),
child: HomeScreen(),
),
BeamPage( //resolves from '/books'
key: ValueKey('books'),
child: BooksScreen(),
),
BeamPage( //resolves from '/books/:bookId'
key: ValueKey('book-${state.pathParameters['bookId']}'),
child: BookDetailsScreen(
bookId: state.pathParameters['bookId'],
),
];
@idkq When you say "SimpleBeamLocation", are you referring to the SimpleBeamLocation
that is a part of beamer? This special BeamLocation
is used by SimpleLocationBuilder
that does exactly what you describe; determines the appropriate BeamPage
s to put into stack by examining path segments (+ /
as first page)
There of course are theoretical scenarios that really require customizing and changing the page stack differently than "path segment = page", so it's good to have flexibility. For example, depending on some context
or query parameter, you may wish to show just the top-most page for some "deep URI", or even show a different page for the same path segment string.
But from my and users experience, most applications follow the "path segment = page" idea and most users really just use SimpleLocationBuilder
.
@slovnicki Got it, exactly.. you are ahead on the game! Just saw in your example here https://github.com/flutter/uxr/blob/master/nav2-usability/scenario_code/lib/deeplink-pathparam/deeplink_pathparam_beamer.dart
On BookstoreNavigator, selectedBook and selectedAuthor are driving the navigation stack: it dictates which screens will be stacked - this makes no sense to me. First of all, there is no 'selected' book or author concept. Instead, the user is simply 'viewing' a book page or author page. The difference is important because if we treat every (or some) page-parameters as conceptual active/inactive object, things can get messy.
The if (selectedBook != null)
statement could be replaced with if (pathTemplate == '/book/:bookId')
- would that make more sense?
Secondly, the only consumers of selectedBook and selectedAuthor are their respective screens, similarly to a single web-page request, this should be scoped down, and not up. Why do we need to parse and use it in the navigator? We need it only to put the MaterialPage into the stack. So, selectedBook and selectedAuthor are not being used anywhere else in the app except their own screens. It means that as the app grows it would have several useless variables and ifs in this BookstoreNavigator class for no reason. If only BookDetailsScreen cares about param book, it should be limited to that scope and not other parts of the app.
You could certainly make BookDetailsScreen
and AuthorDetailsScreen
obtain the selected book / author, instead of having BookstoreNavigator provide them.
An argument can be made that another process could change the state of that variable hence it would activate the screen. But this is not what is happening in this app and it is an unlikely use case overall. As an analogy, you are currently reading this GitHub webpage. Changing the state of a parameter of another webpage does not rebuild your navigation stack.
I don't think this was the reasoning. The reason it works this ways is that AuthorDetailsScreen
requires an Author
, so someone needs to provide that. It just so happens that BookstoreNavigator
is building the AuthorDetailsScreen
. Of course AuthorDetailsScreen
could obtain the selected author based on the route ID obtained by routeState.route.parameters['authorId']
and then obtain the selected author from LibraryScope.of(context).allAuthors
, but as it's currently written the AuthorsScreen is decoupled from the navigation state.
A better approach in my view would be the navigation stack be built based on the active RouteStateScope path hierarchy. Think of it as a Site Map structure.
This is what packages like vrouter, beamer, and auto_route do.
For that to happen a linkage of some sort would need to be implemented, such as 'parent' for each page. So that when pop() is used it moves to the 'parent' route. That could also be used on the outer Navigator in BookstoreScaffoldBody to replace the ifs. In case there are no parents, show the main view.
I'm not sure what you mean by this. Are you talking about how onPopPage
would be implemented?
The if (selectedBook != null) statement could be replaced with if (pathTemplate == '/book/:bookId') - would that make more sense?
Yes. But I rather have it done automatically by another class and getting rid of the conditions entirely. So it can be reused in any app.
Here's an example https://github.com/idkq/samples -> navigation_and_routing
Take a look at the newly created class SimpleNavigator
. Few comments:
Navigator
The reason it works this ways is that AuthorDetailsScreen requires an Author, so someone needs to provide that.
Yeah, in the example I pasted, the AuthorDetailsScreen
would require an Author
in the constructor even if not being used, which led to a hack as you can see in the example.
Take a look at the newly created class SimpleNavigator.
Most of the higher-level packages that were studied abstract away the Page
objects too, not just the Navigator (beamer uses BeamPage
and vrouter uses VWidget
for example). Something to consider, since using the key needs to match the route as currently written.
Yeah, in the example I pasted, the AuthorDetailsScreen would require an Author in the constructor even if not being used, which led to a hack as you can see in the example.
This is why its better to only build the page objects when they are needed. In your version, each page is being constructed because the if (selectedBook)
statements were removed.
@johnpryan It was a quick (and honestly dirty) example I posted, just to demonstrate. Here is a better updated version. https://github.com/idkq/samples - no keys dependency, instead abstracting Page.
All logic is automatic and behind the scenes. No need for the ifs and stacking manually. So the user code is cleaner and easier to maintain as the app grows.
StackingLogic
can be subclassed and customized to handle complex cases. In this demo, there aren't any L2+ multi level stacks but in the real world almost certainly there are. Here is what I mean by multi level stack:
Here is the entire app Tree Map:
There are two Navigators: Outer / Inner Navigators
The most we have is two stacked screens in one nav on BooksDetail and AuthorDetail. Still they are not really two levels because scaffold is always present by default. No logic is needed to decide to show/not show a scaffold. So for this demo, stacking is straightforward.
A 'truly two levels' would be (three levels in total), for example:
Here '/author/:authorId/profile' must be stacked after '/author/:authorId'. When popped '/author/:authorId/profile' must return to '/author/:authorId'. To automate I see two ways/approaches (open to suggestions on other approaches):
AncestorStackingLogic
ParentStackingLogic
.SimpleNavigator
requires a StackingLogic
class in which it delegates the stacking algo.
SIDE NOTE: when using push & link, things are not working properly on the original code. I'm not sure if that use intentionally to show that push does not have the Route info or if it is a bug. Reproduce:
Link does not work. This happen because in AuthorDetailsScreen
you uses push. When mixing push and link the app does not behave correctly.
Maybe out of place, but how would one use snack bars with the navigation?
A common scenario is you confirm your email after signup by following a link back the app. At that point I notify the person that their email is verified and redirect them to the authenticated app page.
Notifying with a snack bar seems most appropriate. You don't need them to take and action and want to get the started ASAP.
But doing it in nav 2.0 does not seem straight forward. Single data point; I haven't figured it out in a few hours spent.
EDIT 1: Obvously the snackbar messenger seems key but with MaterialApp.router how does one access a build context with materialapp in the routePath method? I guess I will just make another one that is accessible....
EDIT 2: Awkward:
final delegate = Delegate();
runApp(
MaterialApp.router(
builder: (context, child) {
delegate.context = context;
return child!;
},
title: 'MyApp',
routerDelegate: delegate,
routeInformationParser: Parser(),
),
);
Regarding pre-loading the next screen, are we mostly interested in pre-rendering that screen, or making sure the data has been loaded?
Rendering should be pretty snappy, so if it's data, then my issue / question about inherited data for child routes might have some overlap:
Closing this issue, since the research has completed.
If you have any feedback on the scope of the Nav 2 API usability project and the approach we are taking, please leave your comments below. I encourage you to check out the project's overview page, which might have answers to your questions. Thanks!