-
Notifications
You must be signed in to change notification settings - Fork 28
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
Declarative vs. imperative routing API #15
Comments
I think the question of a "declarative API" is quite vague. To some degree we all support some kind of declarative programming. I will try to explain:
This is my own ideas, after a lot of research and experimentation. However I might be proven wrong and change my opinion! What are the different types of declarative programming that we can use for navigationI think there are 3 different types:
Here is a description of each of them, along-side code snippet. 1. Navigation Guards (Rerouting based on the state)The idea here is that we navigate using the url, but if we try to access a url that we are not supposed to, we can change the url again to go to a valid one. Here is an example (using vrouter but it does not matter): VRouter(
routes: [
VStacked(
path: '/login',
widget: LoginPage(),
),
VStacked(
beforeEnter: (vRedirector) => !state.isLoggedIn ? vRedirector.push('/login') : null,
path: '/home',
widget: HomePage(),
),
],
) 2. Changing in the routes based on the stateWhile (1) uses the state, the routes do not change in a declarative manner. This is what this second point is about, and you will start to see that it starts to resembles Navigator 2.0 declarative navigation. VRouter(
routes: [
VStacked(
path: '/login',
widget: LoginPage(),
),
if (state.isLoggedIn)
VStacked(
path: '/home',
widget: HomePage(),
),
],
) Here the route "/home" does not exist if 3. Using the state as a declarative way to navigateThis is basically the example you proposed. The state is what determines the navigation. Navigator(
pages: [
MaterialPage(
child: HomePageWidget(),
),
if (state.searchQuery != null)
MaterialPage(
child: SearchPageWidget(),
),
if (state.stockSymbol != null)
MaterialPage(
child: DetailsPageWidget(),
),
],
) What vrouter supportsScenario 1: VRouter, as many of the other navigation packages, supports this scenario. Scenario 2: VRouter does not currently supports this scenario, but might in the future. Scenario 3: VRouter does not support this scenario and it is not planned. I will explain why in the next part. If I am proven wrong this statement could be invalidated of course. My opinion on navigator 2.0 declarative routing (Scenario 3)First, I want to say that this kind of routing is amazing. This is one of the main reasons I had a look into Navigator 2.0 and I think it is brilliant. However, I don't think this is viable for a cross-platform framework. Indeed, if we want easy cross-platform compatibility, we must agree that the way developer navigate should be very similar in scenario 1, 2 and 3.
Basically:
The means that, if we want our package usage to be platform agnostic we can either support only "url -> state" OR "state -> url" and "url -> state". You might say that we can support "state -> url" for mobile and allow both when we are on the web. However this is not at all platform agnostic and adding "url -> state" afterwards (when converting a mobile app to a web app for example) would be a lot of work that a developer did not take into account and which might even change its app architecture. I hope I was clear, I have been looking into it for some time now, not only from the perspective of flutter but also by looking what other framework do. I do think state management and navigation should interact but not be blended. |
@InMatrix beamer provides the same declarative approach to building the list of pages as Pages API. In beamer, you override from https://github.com/slovnicki/beamer/blob/master/example/lib/main.dart (although not using @override
List<BeamPage> pagesBuilder(BuildContext context) => [
if (pathSegments.contains('books'))
BeamPage(
key: ValueKey('books-${queryParameters['title'] ?? ''}'),
child: BooksScreen(),
),
if (pathParameters.containsKey('bookId'))
BeamPage(
key: ValueKey('book-${pathParameters['bookId']}'),
child: BookDetailsScreen(
bookId: pathParameters['bookId'],
),
),
]; The only imperative-like APIs are Although there can be just a single class BooksLocation extends BeamLocation {
@override
List<String> get pathBlueprints => [
'/books/:bookId/genres/:genreId',
'/books/:bookId/buy',
];
@override
List<BeamPage> pagesBuilder(BuildContext context) => [
if (pathSegments.contains('books'))
BeamPage(
key: ValueKey('books-${queryParameters['title'] ?? ''}'),
child: BooksScreen(
titleQuery: queryParameters['title'] ?? '',
),
),
if (pathParameters.containsKey('bookId'))
BeamPage(
key: ValueKey('book-${pathParameters['bookId']}'),
child: BookDetailsScreen(
bookId: pathParameters['bookId'],
),
),
if (pathSegments.contains('buy'))
BeamPage(
key: ValueKey('book-${pathParameters['bookId']}-buy'),
child: BuyScreen(
book: data['book'],
),
),
if (pathSegments.contains('genres'))
BeamPage(
key: ValueKey('book-${pathParameters['bookId']}-genres'),
child: GenresScreen(
book: data['book'],
),
),
if (pathParameters.containsKey('genreId'))
BeamPage(
key: ValueKey('genres-${pathParameters['genreId']}'),
child: GenreDetailsScreen(
genre: data['genre'],
),
),
];
}
class ArticlesLocation extends BeamLocation {
@override
List<String> get pathBlueprints => ['/articles/:articleId'];
@override
List<BeamPage> pagesBuilder(BuildContext context) => [
...HomeLocation().pagesBuilder(context),
if (pathSegments.contains('articles'))
BeamPage(
key: ValueKey('articles'),
child: ArticlesScreen(),
),
if (pathParameters.containsKey('articleId'))
BeamPage(
key: ValueKey('articles-${pathParameters['articleId']}'),
child: ArticleDetailsScreen(
articleId: pathParameters['articleId'],
),
),
];
} |
@lulupointu Thanks for pointing that out, I never thought of how declarative routing might work on mobile vs web in respect to whether To me, Scenario 2 doesn't really resemble declarative routing at all. It's more just creating and destroying routes based on state like you mentioned. However, to your point about Scenario 3 - I think changing routes based on state is just too powerful of a tool, and in my opinion we should definitely add it into the UXR Storybook. To get around your point about cross-platform navigation and The following routes are defined:
Now, "/sign_up" is the start of the flow (home page of the flow). On mobile, everything can work normally like you mentioned. Now, if you are on web and you try to go directly to any of the other routes like "/sign_up/password", but the state says you should be at "/sign_up", you just simply get auto-redirected back to "/sign_up". I feel like this should be the default behaviour for declarative routing on web. So basically on web, initially I know that blending state and navigation is not standard, and I haven't seen it done in other frameworks. But it feels like this could be a new feature and eventually a standard for navigation packages in Flutter, especially since navigation and state are completely intertwined for flows. Furthermore, the Navigator 2.0 API in Flutter makes this much easier to implement than other languages. Please let me know all your thoughts though! |
As you said, given your example, we would have to implement Actually if you look into the example of @slovnicki you see that he is not implementing If you want a library that handle both navigation and state 100% I guess that would be theoretically possible, but:
|
@lulupointu @theweiweiway Interesting discussion about What is the situation in app's code when state needs to drive your navigation and you cannot intervene and say: ok, this happened so we should head over to route (obviously, we need |
I agree with you, I do not think there is a situation where you would want that. This is why I think:
|
Or maybe we just want to append some parameters to As recently proposed here |
This is definitely feasible. However this might press user into confusing navigation and state management so I don't think I would personally go for it. I understand the example you gave but I would personally still go for a push, which given the fact that the path of the url does not change would not update the app visually but will just append the given parameter. |
Hey all, great discussion, to clear up some things:
This type of routing is really only for flows. The Flow Builder package by felangel really highlights this well, where routes need to change based on state. I feel like this scenario happens in almost every app, web or mobile. Because chances are that users will need to fill out a flow or form at some point.
AutoRoute supports this, please see: https://github.com/Milad-Akarie/auto_route_library/blob/master/auto_route/example/lib/mobile/screens/user-data/data_collector.dart
On, no no. We definitely do not want to handle navigation and state together. That would be an absolute nightmare disaster. If you take a look at the AutoRoute example above, there is no baked state into the package. It's relying on Provider
My thinking was that the developer doesn't need to do anything. By default, the package would handle this, and it would redirect the user to the correct path if the state requirements are not met. My gut feeling is that AutoRoute does this already, but I will need to test it. I'll get back to everyone with those results later today! |
I'm building a mid-to-large app and initially was using 100% declarative (not using any package). I ended up creating dozens of state variables with This is true especially for static screens (information only display). Screen A -> Screen B. How do you control the state of what screen is being shown. To have a variable to for this became a real nightmare. Unless there is a better/genius way to do this? I'm still using declarative but only have 1 variable per bottomnavigator that controls all screens inside (basically an index). It is a bit weird, but much better than initially. In your example @InMatrix you have to (somehow) remember that:
|
I am with @theweiweiway, we should add it to the UXR Storybook. |
Thanks to everyone who has provided input here. To clarify, I was referring to the third situation "using the state as a declarative way to navigate" mentioned by @lulupointu with the term "declarative routing API". It seems that the main issue here is whether routing should be driven by URL or app state based on the above discussion. In addition, @slovnicki seems to propose a middle ground, where routing to major locations in an app is URL driven, but local transitions can be driven by state variables. As an argument against (full?) state-based routing, @idkq points out that managing a large number of state variable for controlling which page is currently visible is messy. That seems like a valid point. @SchabanBo and @theweiweiway could you give a concrete example where you think a state-based routing API is advantageous?
I understand the desire, but the scenarios in the storyboard deck describe end-user interactions with an app. Those scenarios don't seek to specify how they should be implemented. As a next step (#9), we'll invite package authors to provide sample code for implementing those scenarios, and there will be a process to critique implementations using different APIs. The best way to advocate for a state-based declarative routing API is to propose a user-facing navigation scenario where such an API can show its distinctive advantage over the alternative. |
Just to expand a bit, from my experience, issues with declarative (@lulupointu scenario 3):
I am not saying it is all bad, but we can find ways to improve. |
Absolutely! I believe it is totally achievable. The clear advantage of declarative is that your app is self-aware of navigation in a reactive way: as object's states changes, your app navigation's change automatically. The challenge is how to blend these events/state/urls in a robust but easy way. |
I see, sorry for encroaching
To show you the advantages, please consider the following scenario. An auth sign up flow with the following screens:
Now, let's assume each page has a next button that pushes the user to the next page of the sign up flow. ...
ElevatedButton(
child: Text("Next"),
onPressed: () {
saveFormDataToState(data);
router.push(<NEXT_PAGE>); // we have to push the next page on each one of our pages
}
)
... We need have a Here's the example in a declarative fashion: ...
ElevatedButton(
child: Text("Next"),
onPressed: () {
saveFormDataToState(data);
// router.push(<NEXT_PAGE>); we can now take this line out
}
)
... where the declarative router looks something like this: ...
return AutoRoute.declarative(
pages: [
SignUpPage(),
if (state.email != null) SignUpEmailPage(),
if (state.username != null) SignUpUsernamePage(),
if (state.password != null) SignUpPasswordPage(),
if (state.age != null) SignUpAgePage(),
if (state.gender != null) SignUpGenderPage(),
],
);
... Whoop-di-do. So what. We saved 1 line of code in each of our pages and added a whole chunk of code for the declarative router, which is hardly great. However, this really makes things much much easier to work with in terms of maintainability and modification of the code. If I wanted to add another route, I just stick it in the appropriate place in the declarative router. That's it. I don't ever need to go to the respective pages themselves and change up the I also have a nice representation of all the pages in this flow, and know exactly when they are in the stack and when they are not. You might be able to see how this becomes very powerful in more complicated flows, where pages should only be shown if criteria A, B, C and D are met. Finally, I'd just like to stress that this kind of routing seems to only good for flows and should probably never be used for the majority of an app. In my experience, it seems like around 20-30% of mobile pages are flow-based? Much less for web though, since you can stuff more form fields onto a web page than a mobile page. But I don't think this defeats the notion of having declarative routing. |
But often a state only changes after an event is triggered. So connecting navigation to state change requires an additional step (conceptual step, not necessarily coded step). Declarative: a) Event occurred -> b) State changed -> c) Navigation change Using declarative I can change the state of something from many places (b) in my app and cause navigation to change. That cannot be done with imperative. |
My main thought here is that URLs are already declarative. I don't understand the benefits of adding an extra app state object to update. My initial biggest issue with implementing Navigator 2.0 was, from the examples give, there were three sets of state to manage:
What's the source of truth with these? They can all be:
In Flutter, we're used to a really clear data flow, it's one of the beautiful things about the framework: State ➡️ Widgets ➡️ Elements ➡️ RenderObjects. From a developer's perspective, the data flow is one-way and really easy to understand. There are single sources of truth. But with having a declarative state object, you've suddenly got three sources of truth, and it's really complicated to think about. What updates what, and when? This, I believe, is the primary source of complexity with Navigator 2.0. Given that URLs are already declarative, and if you're always going to have to handle them, aren't they the easiest way of always declaring the navigator state? |
@tomgilder Basically you have to decide between 1 & 2, which one controls 3. In flutter for web 1 controls 2 which controls 3 in some cases (when url is typed). In others, 2 controls 3 and url is just updated to reflect current url. |
Hey @tomgilder , That is a great point. Having a feature like Flow-based routing (I'm going to stop calling this Declarative routing because you are right, URLs are already declarative) definitely introduces complexity where we now have potentially 3 sources of truth. However, I'm not completely convinced that this statement is true:
I actually think that for flows, URLs are not the easiest way of declaring navigator state. Furthermore, I think that if we can define a clear separation between the 3 cases/sources of truth that you mentioned, we can make it intuitive to the developer - all while keeping the advantages of each case. I'll try to explain: URL source of truthFor most routes in an app. You can push to certain pages, pop from pages as well. This is what everyone is familiar with and is used for most routes. State source of truthFor flows. I truly believe this source of truth is the easiest to work with for flows, where a user has to go through a set of pages to fill in information (like a Profile data collection or sign up flow). The reason I think this, is because flows are so closely linked to state that it makes sense to make the routing change in response to state. In a sign up flow with an: when do we want to navigate to the next page? We want to go to the username page when the email has been added to state. We want to go to the password page when the username has been added to state. We literally want to route to the next page in response to or at the same time the state is updated. Please see my last comment to see my reasoning behind why state-based routing is better than normal URL-based routing for flows. Also, I don't think this case is a one-off thing. Almost every app I can think of uses flows because the end user is almost always required to fill in some sort of information through a flow. Widget source of truthFor bottom navigation bars and similar widgets that change state, resulting in the page to change. Now what?Now, we have our clear separations between each type of routing and when to use them:
I understand there is a little bit of a learning curve. When I first saw this in the Flow Builder and AutoRoute package, I will admit I didn't completely understand what was happening and why I needed it. But then as soon as I started trying Flow-based routing for flows, it just made sense. Finally, I think there is almost some confusion surrounding this because it isn't entirely clear how one might implement URL-based navigation and state-based navigation together in an app. If you take a look at the autoroute docs at https://autoroute.vercel.app, and go through the the last 5 sections, you can see how all 3 sources of truth can co-exist in the same app while being intuitive to the developer. The beauty of the AutoRoute package though, is that you can make a perfectly functional URL-based app like you are suggesting without making anything state-based. But it also opens up the possibility for anyone to tap into the advantages of Flow-based routing as well. |
I think that state navigation would be better if they were by default:
Which is more or less what the URLs are. Any fancy scenario would be handled outside of this rules. |
@theweiweiway I think this is an excellent point. Flows are very different from the rest of navigation, and I agree that it's worth treating them as a separate case. I would argue that often flows don't want a URL change - page 2 probably depends on state from page 1, and too much state to add into a URL. My gut feeling is the best way of dealing with this is to have a nested Navigator with its own list of pages, which is generated from the flow's state. I'm going to experiment with this and see if it makes sense. Something I didn't say above: I think it's easy to make global navigation state objects look like a good idea on a fairly simple example app. But they just don't scale to a complicated real-world one. When I saw the Flutter book example app, my reaction was "oh, cool, it's declarative" - but then trying to bring that to a full app was just too complicated. |
So as another example for the declarative is that I can define the complete stack to show. Navigator(
pages: [
MaterialPage(child: CategoryPage()),
if (user.country == 'Germany') MaterialPage(child: OffersPage()),
MaterialPage(child: ProductPage()),
],
) So I think doing it here as declarative is easier than imperative |
Idea snippet for DeclarativeHere is my idea. I've introduced two classes Navigator(
pages: [
PageOrder( children:[
PageResolver(visibleWhen: ()=> user.country == 'Germany', child: OffersPage())
MaterialPage(child: CategoryPage()),
MaterialPage(child: ProductPage()),
])
],
) Note that two other pages don't have a PageResolver(visibleWhen: ()=> user.country == 'Germany', child: OffersPage(), url: 'root/main/offers/germany' ) And then to PageOrder(orderLogic: OrderLogic.UrlHierarchy) Or the user can customize their own order logic by extending OrderLogic. Other attributes to the What does this solve?
Summary
|
Hey @tomgilder,
Agreed! Initially I was thinking that the URL would change as well, but you are right. Often times, I don't think you really need a URL change for flows. This would also solve the problem @lulupointu mentioned about how to handle url driving state and vice versa for flows. However, one use-case I am able to think of that would require a URL change for flows, is for analytics. If we had a sign-up flow and we wanted to track where the user drops off in the sign up flow, we could just listen to URL changes to figure this out. However, if there are no URL changes for the flow, we would need to manually log an event for each page in the flow. I think tracking where users drop out of a flow is pretty useful, but not sure how much this is done in practice.
Definitely agreed as well - i totally do not think this is a good routing solution if attached to global navigation state since it gets way to complicated, as @idkq realized. That's why this solution I would recommend only to be used with flows, and with your idea of having a nested navigator, you could scope that nested navigator to a piece of local state (like a SignUpBloc). Then, for each additional flow you just have an additional piece of local state. This keeps everything clean, separated, and super easy to use/maintain/modify. |
Nice discussion! I like your ideas a lot! My personal opinion is to keep simple by default and open a hole where developers could easily change the default approach to solve complicated scenarios when needed. My experience is that, if navigation works with Browsers, it will definitely work with Native Apps. Therefore, any navigation solution must consider to work with Web first. Otherwise, cross-platform philosophy will not be satisfied. Now, for some scenarios, maybe developers want to navigate with state. We need to answer some questions:
I didn't try, but https://autoroute.vercel.app/advanced/declarative_routing describes the approach quite similar to what I think about managing navigation state. |
Sure @johnpryan, I will reorganize my imagined API and will share here soon. |
I would recommend strongly against this in most cases. I don't see any benefits on having a hierarchy of stacks hardcoded. Navigation changes frequently and having to update the hierarchy in the code is unnecessary. That's why I suggested Developers need a simple way stating all URLs without a hardcoded hierarchical wiring. |
I documented my work progress here. ExampleLet's start with an example, which is complex enough to see the problem.
High level ideaManaging the whole navigation system in one place Therefore, the idea is splitting into smaller navigation domains (I call them stacks) and combine them into a single
|
@nguyenxndaidev this is literally beamer. Your If you would to continue this idea further, you would realize that instead of The only thing beamer doesn't have yet is a more convenient beamer example: class BooksLocation extends BeamLocation {
@override
List<String> get pathBlueprints => ['/books/:bookId'];
@override
List<BeamPage> pagesBuilder(BuildContext context) => [
...HomeLocation().pagesBuilder(context),
if (pathSegments.contains('books'))
BeamPage(
key: ValueKey('books'),
child: BooksScreen(),
),
if (pathParameters.containsKey('bookId'))
BeamPage(
key: ValueKey('book-${pathParameters['bookId']}'),
child: BookDetailsScreen(
bookId: pathParameters['bookId'],
),
),
];
} By typesafe, I suppose you mean that URL parameters are automatically mapped to correct types. I think this is slightly out of scope (which makes it easier) for navigation package. My only argument here (and I suppose sufficient) is that Uri does also not deal with types. It parses everything as |
I updated my declarative API library (Navi) I am working on. You can check details in the link above. The API is also mocked with real source code here. The code is only to review to know how the API is used in an application, it doesn't work yet. I am starting the implementation. Below is a quick introduction. To use the library, you only need to know how to use 2 simple classes:
If you want to have nested routes, use The example below use Calling
|
I have been working on improving the declarative experience of beamer and I think I found a sweet spot regarding the discussion in this issue. I would like to hear your thoughts on this. I'll give some introduction on core concepts of beamer that will lead to the main point in section 4. 1. Group contextually related page stacks into a
|
Hi everyone, Finally I published the first working version of Navi, a declarative navigation API. Currently, I have only a short documentation and 2 simple working examples to show how it works. More will be added soon to serve the Navigator-2.0-API-Usability-Research. I will concentrate on main functionalities of declarative API before adding imperative API. |
Personally. I handle state within pages via widgets and switch statements. It is far easier to read. Dart can be written in a readable fashion and riverpod/provider helps. Outside the widget tree idiomatic Flutter is often unreadable. The situation even for context free Nav 1.0 is far more complicated than it should be. FWIW a simple context free (key by default) imperative api that enables the back button and url entry by default would be perfect. Actually I am not too bothered about declarative or imperative, it is just that imperative is more likely to offer a one line to switch page and record nav history automatically, without the boilerplate. |
For simple cases, sometime imperative is better than declarative. If you see the examples I provided in Navi package, you will see that
I think you misunderstood the purpose of page hierarchy. Please check Material Navigation Guide to see 3 different navigation types. Nav 1.0 only work with default history navigation (chronological navigation). You cannot control Lateral and Upward navigation with Nav 1.0. And even for the chronological navigation in Nav 1.0, it's too hard to remove previous pages from the history. With Nav 2.0 declarative API, you are having full control for Lateral and Upward navigation. But it has 2 big problems: too difficult to use and no support for chronological navigation. I am planning to solve problems above in the Navi package:
|
I am afraid that I disagree with mostly. Declarative has it's place and so does imperative. Similar to those that think OOP everywhere is optimal are wrong. Like everything, it depends. I believe the material link you provided confuses together global navigation with transitions and local widget switching. If you see them being served by the same tool. Only web will have a url in any case. So the app better work well without it anyway. Finally. You shouldn't have to worry about whether the build has disposed before your navigation state decision (keyed by default instead of build context). |
I don't say that imperative is not good. We will need both, imperative for simple cases and declarative for other cases. The only problem of declarative approach is that, we don't have yet a good API ready to fully compare to imperative. You need to wait until a good declarative idea released to have a final conclusion. When a declarative API is ready, we will need to implement exactly the same scenarios (simple and complex) in both ways. Otherwise, everything we are saying is just theory. |
Well if all that you want to do is navigate to a url then there is no debate. My point is that even the current imperative API could be simplified and work anywhere. Debating declarative vs imperative, distracts from the real issues. Navigation should not be complex, especially by default. I believe e.g. ref tags should be a separate API. It is possible that this stems from a fear of Global state. Global state should be avoided but sometimes it is THE BEST option. Some of the most reliable systems in the world run on nothing but dedicated global state, such as one of green hills embedded os. Atleast base url navigation is inherently global. |
@kevlar700 I plan to support both declarative and imperative in Navi. So comparing these 2 approaches becomes useless if the library gives you both to choose anyway :-). |
@nguyenxndaidev Exactly. Having both imperative and declarative is a holy grail for a navigation package, which I pursue in beamer also. |
Again. despite saying devs prefer imperative myself in the past. I no longer think that imperative or declarative is related to what most developers want by default. They do prefer nav 1 for other reasons. They ideally want to be able to set a global list of routes and navigate to any route from anywhere and have the browser back button and url bar work. Ideally without Any other boilerplate. That is what web browsers deliver with html. I am not sure it is possible without editing flutters navigator code itself, however. Potentially having both declarative and imperative could cause confusion but it shouldn't matter too much. Perhaps they could serve different parts of the API. |
Agree with you all. The challenge is how to make it simple, short, easy to read and use. I haven’t seen any solution yet that is close to this ultimate goal unfortunately, despite everyone’s contribution and effort. The baseline for the new api should be 1 line of code. Or around that. We need a solution. |
Wow, great thread and discussion. On the topic of "Flows" being a good use case for state-based navigation. What is also really well suited to flows is just the classic imperative navigation!
So I think maybe we are overthinking it a bit. I think just some really intuitive clear declarative API for app navigation is what we need, and really the classic imperitive API are more than fine to handle flows. State based declarative nav is mainly good at clearing up confusion around a complex nav stack, where many entities may be pushing and popping the stack. In a |
I think this is the key point, we should be starting from this use case, and working backwards. The web is fundamentally an imperative paradigm with a bookmarking system. It is constantly just calling pushReplacement(), while at the same time, accumulating more and more 'state' in memory that the views can share. Trying to marry this inherently imperative approach with a declarative stacked approach is what causes all the headaches I think. I'm not too sure where I'm going with this, except I guess I agree that making the existing imperative API clearer and more of a first class citizen would be good. What I really want from Flutter are just 3 things:
I think for the most part this is well served already by MaterialApp.onGenerateRoute, but maybe it can be made even easier to use with a dedicated widget that can be stuck anywhere in the tree? Or losing the dependence on PageRoute which has loads of baggage and legacy considerations. Using MaterialApp.onGenerateRoute to navigate is still much more complicated than basic HTML hrefs and .location for example. To be honest, I think the jury is still out on whether declarative is actually better at all in these complex scenarios. I think you can argue it has inherent scaling issues, which imperative doesn't really suffer from. Imperative is more just a constant level of confusion vs an ever growing one :p |
Having played around with VRouter for a bit, I've landed on what I think is my preferred approach, which to NOT have state drive navigation, but rather allow navigation to change state, and to declare all of that in one place inside the routing table. There is no getting around the fact that some navigation changes are event based, not state based. Trying to support both event based and state based quickly ends in a mess. You start creating state, to emulate events, like It seems much more straightforward to keep the actual navigation changes imperative (pushNamed), but then to have some declared logic around those routes that can modify the state. This would be identical to how html has worked for decades. When a page is loaded, args are extracted from the path, and (maybe) injected into the current $_SESSION, which acts as shared state for all pages. All page changes are imperative. If someone lands somewhere they shouldn't be, they are simply re-directed elsewhere. This is a bit of pseudocode, but for example, if we want to show a specific product, that can be deeplinked to, you can have something like:
So here we can go to any product url, and the app state is updated to match. To move around our app we'd have links like: The product page can then simply check the model for the productId, and load what it needs. Or, we could pass it directly if using a view model, something like:
This makes for a very nice mix of declarative and imperitive, with a foot in each. The tree structure and nesting are all declarative, defined in one spot and easy to grokk/change. But the actual changing of views is event based as most developers are used to, and then the updating of state is a mix of declarative callbacks and imperative code inside them. There is no need to consider how to convert your app state to a url, because the data flow only goes one way. All you need to consider, is what state should change when you enter a given path, and what the pre-conditions are for viewing that path. |
I'm happy that you could reach this point of view. I am happy that other are trying different approaches but I still strongly believe that this single flow way of thinking is the best way to navigate. |
This is exactly what I see when I am trying to create a complex scenario with declarative approach. That's why in my plan for For other points in your comment, I will spend more time to evaluate with some ideas to find the best solution. |
I've been using declarative for a while now and the latest challenge I found is regarding paths. Consider these paths:
Using declarative, I can link state <-> screen. Let's assume 'product' is currently visible now. If go 100% declarative-model and want 'details' to be visible then I simply put a condition to trigger/show 'details'. HOWEVER (here is the tricky part) if I want to mix declarative with a bit of imperative, it gets ugly. For example, if I have a enum-holding-variable
I can show 'product'. But to go up & back, now I have two candidates for parent of product: 'checkout' and 'main'. I could store a list of previous visited paths but it is really a lot of work! Long story short: the more I use declarative, the more I reverse course to imperative. |
Hello guys, finally I found an elegant API, which is simple and easy to learn, yet keeping full power of Navigator 2.0 combine with imperative API on top of the core declarative API. The result is published as I hope you will enjoy |
@nguyenxndaidev I like where navi is going. |
Looking at the many packages for routing in Flutter as listed in #13, most of them seem to offer an imperative API similar to Navigator 1.0 (e.g. push, pop). I'm curious why a declarative routing API, a key feature of Navigator 2.0 (see example below), didn't get adopted in those package. Did I miss a package that does offer a declarative routing API?
Here's a link to full example in Navigator 2.0's design doc.
Cc: @slovnicki, @johnpryan, @theweiweiway, @lulupointu, @nguyenxndaidev
The text was updated successfully, but these errors were encountered: