dynamic_routes 1.1.1 dynamic_routes: ^1.1.1 copied to clipboard
A super easy-to-use routing library for Flutter that allows you to determine routes and the order of those routes all in one place.
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
}
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
}
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 theNavigationLogicProvider
, 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();
}
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
}
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),
)
]
);
}
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
});
}
);
}
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);
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();
})
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]
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);
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);
}
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;
}
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();
}
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!));
}
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.
customNextCallback
and 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);
}
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);
}