0 purchases
dynamic routes
Dynamic Routes #
Dynamic Routes is a library that lets you specify in advance which routes should be shown and in
what order, from just one place in your code. This is invaluable for flow management -- when you want some routes to show, or their order swapped, based on some information that you obtain during runtime.
A good example of such a flow would be a registration flow where, based some information in the database -- whether this user has already registered with your company before, where they live, etc. -- only the required pages are shown.
Overview #
Note1: I'll be using the words "widget", "page", and "route" interchangeably
Note2: I recommend that all your pages pass data to one another through a centralized cache for more modularity. This library provides a simple caching method that is scoped to each initiator. But you are welcome to use other solutions.
This library comprises two main parts, the Initiator, and the Participator.
The Initiator page is a page immeidately before where you want your dynamic navigation flow to happen. We'll put all the navigation logic in this page. This could be, for example, a landing page.
// landing_page.dart
class _SomeWidgetState extends State<SomeWidget> with DynamicRoutesInitiator {
//...some code
void onButtonPressed() {
const isPage4Required = calculateIfPage4IsRequired();
final routes = [
Page1(),
Page2(),
Page3(),
if (isPage4Required) Page4(),
Page5(),
];
dynamicRoutesInitiator.initializeRoutes(
routes,
lastPageCallback: (context) {
// Do something; maybe return to homepage.
}
);
// This will push the first Participator page.
dynamicRoutesInitiator.pushFirst(context);
}
//...some code
}
copied to clipboard
The pages in the routes array are Participator pages. These pages do not have navigation logic in them, the only thing they know is when to go to the next page and when to go back.
// page1.dart
class _SomeWidgetState extends State<SomeWidget> with DynamicRoutesParticipator {
// pushNext tells this participator page to push the next page in the routes array we saw earlier.
void onNextButtonPressed() => dynamicRoutesParticipator.pushNext(context);
// popCurrent tells this participator page to pop the current page and all of its sub-routes.
// In some cases, this is the same as using Navigator.of(context).pop(context)
void onBackButtonPressed() => dynamicRoutesParticipator.popCurrent(context);
//...build methods and whatever
}
copied to clipboard
A bit more about popCurrent #
popCurrent behaves similiarly to using Navigator.of(context).pop for most usecases.
But unless necessary, use popCurrent instead because:
popCurrent's implementation is bound to the NavigationLogicProvider, which can be overridden (see the extending navigation logic section).
This will guarantee that the page being popped is the current page.
In the next section, we'll show how you can do nested navigation with this library. In a
nested navigation, as you can imagine, a participator can also have its own flow, and using
Navigator.of(context).pop will pop the page at the top of the stack instead of popping the current
participator page.
But, all in all, using Navigator.of(context).pop will not break your app. By default, the back button uses the pop method anyway.
Disposing the Initiator and the Participators #
We can dispose the DynamicRoutesInitiator instance along with the page itself by calling the
Initiator's dispose method in the state's dispose method. This will also dispose all
DynamicRoutesParticipator instances.
@override
void dispose() {
dynamicRoutesInitiator.dispose();
super.dispose();
}
copied to clipboard
Nested Navigation #
You can also have a sort of sub-routing navigation, where for example, the second member in the
Initiator array is also an Initiator and can branch off into its own dynamic navigation flow.
To do this, we simply mark the state of the second page with both the Participator and the Initiator mixins.
class _MixedPageState extends State<MixedPage>
with DynamicRoutesParticipator, DynamicRoutesInitiator {
// Some code
}
copied to clipboard
And then we can use either the Initiator or the Participator instances when appropriate.
Widget buildButtons() {
return Column(
children: [
TextButton(
child: Text("Click this to branch off"),
onPressed: () {
dynamicRoutesInitiator.initializeRoutes(const [
ParticipatorPage(title: "SubFlow 1 Sub page 1"),
ParticipatorPage(title: "SubFlow 1 Sub page 2"),
ParticipatorPage(title: "SubFlow 1 Sub page 3"),
], lastPageCallback: (context) {
dynamicRoutesInitiator.popUntilInitiatorPage(context);
// Or if you do this, this page, and all of the pages in the subflow that branched off
// from this page, will be popped. Internally, we're using popUntil
// dynamicRoutesParticipator.popCurrent(context);
});
}
),
TextButton(
child: Text("Click this to continue the flow"),
onPressed: () => dynamicRoutesParticipator.pushNext(context),
)
]
);
}
copied to clipboard
Doubly-Nested Navigation #
I don't know when or where or why someone might need this, but as a result of the lib's
route-scoping, you can also have a subflow within another subflow.
Widget buildButtons() {
return TextButton(
child: Text("Click this to branch off"),
onPressed: () {
dynamicRoutesInitiator.initializeRoutes(const [
// Where SubflowPage class is both a navigator and an initiator.
SubflowPage(pages: [
Page1(),
Page2(),
Page3(),
SubflowPage(pages: [
Page1(),
if (page2Required) Page2(),
if (page4BeforePage3) ...[Page4(), Page3()] else
[
Page3(),
Page4(),
]
])
]),
ParticipatorPage(title: "SubFlow 1 Sub page 3"),
], lastPageCallback: (context) {
// Do whatever
});
}
);
}
copied to clipboard
Multi-page Navigation #
pushFor #
You can push multiple pages at once with pushFor.
This method guarantees that you will never push beyond the last Participator page.
// Pushes 4 pages.
dynamicRoutesParticipator.pushFor(context, 4);
// Pushes to the last participator page.
dynamicRoutesParticipator.pushFor(context, dynamicRoutesParticipator..getProgressFromCurrentPage());
// Pushes to the last participator page + invoke [lastPageCallback].
dynamicRoutesParticipator.pushFor(context, dynamicRoutesParticipator..getProgressFromCurrentPage() + 1);
copied to clipboard
The method returns a list of Future of results from each of the pages; you can await all of them like
so:
// Assume that we are in the first participator page.
final results = await Future.wait(dynamicRoutesParticipator.pushFor(context, 3));
print(results); // [resultFromSecond, resultFromThird, resultFromFourth];
setState((){
updateWhatever();
})
copied to clipboard
The method is only available to the dynamicRoutesParticipator instances. For a similar functionality for dynamicRoutesInitiator, use pushFirstThenFor.
pushFirstThenFor #
This is similar to pushFor, but is called from the initiator. Internally, it's pushFirst + pushFor back to back. All methods of awaiting the results mentioned above apply here as well.
dynamicRoutesInitiator.initializeRoutes(...);
// This will push the first page, then push 3 more pages. We are basically pushing a total of 4 pages.
final results = await Future.wait(dynamicRoutesInitiator.pushFirstThenFor(context, 3));
print(results); //[resultFromFirst, resultFromSecond, resultFromThird, resultFromFourth]
copied to clipboard
popFor #
You can reset the flow, eg. go back to the first Participator page, or the Initiator page with popFor.
popFor guarantees that you will never pop beyond the Initiator page.
// Pop just 2 pages while returning true as the result to those two pages.
dynamicRoutesNavigator.popFor(context, 2 , true);
// This pops until the first participator page.
final currentPageIndex = dynamicRoutesNavigator.getCurrentPageIndex();
dynamicRoutesNavigator.popFor(context, currentPageIndex);
// Add + 1 to currentPageIndex or just use double.infinity to pop to the Initiator page.
dynamicRoutesNavigator.popFor(context, currentPageIndex);
dynamicRoutesNavigator.popFor(context, double.infinity);
copied to clipboard
Caching #
This library also supports a simple caching method.
You can call this whenever, and wherever, from both the Participators and Initiator pages.
void saveToCache(WhatEverClassThisThingIs someData) {
dynamicRoutesParticipator.setCache(someData);
// Or
dynamicRoutesInitiator.setCache(someData);
}
copied to clipboard
Once set, this can be accessed from all members of the navigation.
Whatever readFromCache() {
return dynamicRoutesParticipator.getCache() as Whatever;
}
// Or
Whatever readFromCache() {
return dynamicRoutesInitiator.getCache() as Whatever;
}
copied to clipboard
By default, cache data gets cleared along with dynamicRoutesInitiator when the dispose method is called. This can be overridden directly from the method with the clearCache argument.
@override
void initState() {
dynamicRoutesInitiator.dispose(clearCache: false); // true by default.
super.initState();
}
copied to clipboard
Modifying, extending, or replacing the navigation logic. #
It is possible to partly, or completely supplant or modify the navigation logic. If you want, for
example, to do something everytime pushNext or pop is called, you can implement the
NavigationLogicProvider class or its implementation, and provide yours as the new
navigationLogicProvider.
_Note that setNavigationLogicProvider does not override the internal checks.
// An example from pushFirst source code.
@override
Future<T?> pushFirst<T>(BuildContext context) {
assert(
_isStackLoaded,
"the initializeRoutes() method should be called first before this can "
"be used.");
final firstPage = _pageDataMap.values.first;
_widget = firstPage.widget;
// This is what you are overriding; everything above stays the same.
return _navigationLogicProvider
.next(NextArguments(context: context, nextPage: _widget!));
}
copied to clipboard
In the first example, we replaces the navigation logic completely. #
Instead of calling Flutter's Navigator.of(context).push, we just swap out the current widget with
a new one.
customNextCallbackand customBackCallback are just methods that I added to this class so that we
can pass it custom implementation from elsewhere.
// Create a new class that extends NavigationLogicProvider.
class CustomNavigationLogicProvider implements NavigationLogicProvider {
final Function(Widget) customNextCallback;
final Function(Widget?) customBackCallback;
const CustomNavigationLogicProvider(
{required this.customNextCallback, required this.customBackCallback});
@override
void back<T>(args) {
customBackCallback(args.previousPage);
}
@override
Future<T?> next<T>(args) async {
customNextCallback(args.nextPage);
return null;
}
}
// ... somewhere inside your Initiator widget
late final CustomNavigationLogicProvider _customNavigationLogicProvider;
void initiateDynamicRoutesInstane() {
// Initialize normally
dynamicRoutesInitiator.initializeRoutes(_widgets,
lastPageCallback: (newContext) {
dynamicRoutesInitiator.popUntilInitiatorPage(context);
});
final customNavigationLogicProvider = CustomNavigationLogicProvider(
customNextCallback: (Widget widget) {
setState(() {
_displayedWidget = widget;
});
}, customBackCallback: (Widget? maybeAWidget) {
setState(() {
_displayedWidget = maybeAWidget;
});
});
// Again, make sure this is called after initializeRoutes.
dynamicRoutesInitiator.setNavigationLogicProvider(_customNavigationLogicProvider);
dynamicRoutesInitiator.pushFirst(context);
}
copied to clipboard
In this second example, we extend the already existing implementation and log to firebase every time a navigation occurs. #
// Create a new class that extends the implementation of NavigationLogicProvider
class CustomNavigationLogicProvider extends NavigationLogicProviderImpl {
const CustomNavigationLogicProvider();
@override
Future<T?> next<T>(NextArguments args) async {
// Add the extra functionality(-ies) that we want
logsToFireBase("forward");
return super.next(args);
}
@override
void back<T>(BackArguments args) {
// Add the extra functionality(-ies) that we want
logsToFireBase("back");
super.back(args);
}
}
// ... somewhere inside your Initiator widget
void initiateDynamicRoutesInstance() {
// Initialize normally
dynamicRoutesInitiator.initializeRoutes(_widgets,
lastPageCallback: (newContext) {
// Go back to home page.
Navigator.popUntil(newContext, (route) => route.isFirst);
});
final customNavigationLogicProvider = CustomNavigationLogicProvider();
// Make sure this is called after initializeRoutes.
dynamicRoutesInitiator.setNavigationLogicProvider(customNavigationLogicProvider);
dynamicRoutesInitiator.pushFirst(context);
}
copied to clipboard
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.