APS Navigator – App Pagination System
This library is just a wrapper around Navigator 2.0 and Router/Pages API that tries to make their use easier:
Basic feature set
What we’ve tried to achieve:
- Simple API
- Easy setup
- Minimal amount of “new classes types” to learn:
- No need to extend(or implement) anything
- Web support (check the images in the following sections):
- Back/Forward buttons
- Dynamic URLs
- Static URLs
- Recover app state from web history
- Control of Route Stack:
- Add/remove Pages at a specific position
- Add multiples Pages at once
- Remove a range of pages at once
- Handles Operational System events
- Internal(Nested) Navigators
What we didn’t try to achieve:
- To use code generation
- Don’t get me wrong. Code generation is a fantastic technique that makes code clear and coding faster – we have great libraries that are reference in the community and use it
- The thing is: It doesn’t seems natural to me have to use this kind of procedure for something “basic” as navigation
- To use Strongly-typed arguments passing
Overview
1 – Create the Navigator and define the routes:
final navigator = APSNavigator.from( routes: { '/dynamic_url_example{?tab}': DynamicURLPage.route, '/': ... }, );
2 – Configure MaterialApp to use it:
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp.router( routerDelegate: navigator, routeInformationParser: navigator.parser, ); } }
3 – Create the widget Page (route):
class DynamicURLPage extends StatefulWidget { final int tabIndex; const DynamicURLPage({Key? key, required this.tabIndex}) : super(key: key); @override _DynamicURLPageState createState() => _DynamicURLPageState(); // Builder function static Page route(RouteData data) { final tab = data.values['tab'] == 'books' ? 0 : 1; return MaterialPage( key: const ValueKey('DynamicURLPage'), // Important! Always include a key child: DynamicURLPage(tabIndex: tab), ); } }
- You don’t need to use a static function as PageBuilder, but it seems to be a good way to organize things.
- Important: AVOID using ‘const‘ keyword at
MaterialPage
orDynamicURLPage
levels, or Pop may not work correctly with Web History. - Important: Always include a Key.
4 – Navigate to it:
APSNavigator.of(context).push( path: '/dynamic_url_example', params: {'tab': 'books'}, );
- The browser’s address bar will display:
/dynamic_url_example?tab=books
. - The
Page
will be created and put at the top of the Route Stack.
The following sections describe better the above steps.
Usage
1 – Creating the Navigator and defining the Routes:
final navigator = APSNavigator.from( // Defines the initial route - default is '/': initialRoute: '/dynamic_url_example', // Defines the initial route params - default is 'const {}': initialParams: {'tab': '1'}, routes: { // Defines the location: '/static_url_example' '/static_url_example': PageBuilder.., // Defines the location (and queries): '/dynamic_url_example?tab=(tab_value)&other=(other_value)' // Important: Notice that the '?' is used only once '/dynamic_url_example{?tab,other}': PageBuilder.., // Defines the location (and path variables): '/posts' and '/posts/(post_id_value)' '/posts': PageBuilder.., '/posts/{post_id}': PageBuilder.., // Defines the location (with path and query variables): '/path/(id_value)?q1=(q1_value)&q2=(q2_value)'. '/path/{id}?{?q1,q2}': PageBuilder.., // Defines app root - default '/': PageBuilder.., }, );
routes
is just a map between Templates
and Page Builders
:
-
Templates
are simple strings with predefined markers to Path ({a}
) and Query({?a,b,c..}
) values. -
Page Builders
are plain functions that return aPage
and receive aRouteData
. Check the section 3 bellow.
Given the configuration above, the app will open at: /dynamic_url_example?tab=1
.
2 – Configure MaterialApp:
After creating a Navigator, we need to set it up to be used:
- Set it as
MaterialApp.router.routeDelegate
. - Remember to also add the
MaterialApp.router.routeInformationParser
:
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp.router( routerDelegate: navigator, routeInformationParser: navigator.parser, ); } }
3 – Creating the widget Page(route):
When building a Page
:
- The library tries to match the address
templates
with the current address. E.g.:- Template:
/dynamic_url_example/{id}{?tab,other}'
- Address:
/dynamic_url_example/10?tab=1&other=abc
- Template:
- All paths and queries values are extracted and included in a
RouteData.data
instance. E.g.:{'id': '10', 'tab': '1', 'other': 'abc'}
- This istance is passed as param to the
PageBuilder
function –static Page route(RouteData data)
… - A new Page instance is created and included at the Route Stack – you check that easily using the dev tools.
class DynamicURLPage extends StatefulWidget { final int tabIndex; const DynamicURLPage({Key? key, required this.tabIndex}) : super(key: key); @override _DynamicURLPageState createState() => _DynamicURLPageState(); // You don't need to use a static function as Builder, // but it seems to be a good way to organize things static Page route(RouteData data) { final tab = data.values['tab'] == 'books' ? 0 : 1; return MaterialPage( key: const ValueKey('DynamicURLPage'), // Important! Always include a key child: DynamicURLPage(tabIndex: tab), ); } }
4 – Navigating to Pages:
Example Link: All Navigating Examples
4.1 – To navigate to a route with query variables:
- Template:
/dynamic_url_example{?tab,other}
- Address:
/dynamic_url_example?tab=books&other=abc
APSNavigator.of(context).push( path: '/dynamic_url_example', params: {'tab': 'books', 'other': 'abc'}, // Add query values in [params] );
4.2 – To navigate to a route with path variables:
- Template:
/posts/{post_id}
- Address:
/posts/10
APSNavigator.of(context).push( path: '/post/10', // set path values in [path] );
4.3 – You can also include params that aren’t used as query variables:
- Template:
/static_url_example
- Address:
/static_url_example
APSNavigator.of(context).push( path: '/static_url_example', params: {'tab': 'books'}, // It'll be added to [RouteData.values['tab']] );
Details
1. Dynamic URLs Example
Example Link: Dynamic URLs Example
When using dynamic URLs, changing the app’s state also changes the browser’s URL. To do that:
- Include queries in the templates. E.g:
/dynamic_url_example{?tab}
- Call
updateParams
method to update browser’s URL:
final aps = APSNavigator.of(context); aps.updateParams( params: {'tab': index == 0 ? 'books' : 'authors'}, );
- The method above will include a new entry on the browser’s history.
- Later, if the user selects such entry, we can recover the previous widget’s
State
using:
@override void didUpdateWidget(DynamicURLPage oldWidget) { super.didUpdateWidget(oldWidget); final values = APSNavigator.of(context).currentConfig.values; tabIndex = (values['tab'] == 'books') ? 0 : 1; }
What is important to know:
- Current limitation: Any value used at URL must be saved as
string
. - Don’t forget to include a
Key
on thePage
created by thePageBuilder
to everything works properly.
2. Static URLs Example
Example Link: Static URLs Example
When using static URLs, changing the app’s state doesn’t change the browser’s URL, but it’ll generate a new entry on the history. To do that:
- Don’t include queries on route templates. E.g:
/static_url_example
- As we did with Dynamic’s URL, call
updateParams
method again:
final aps = APSNavigator.of(context); aps.updateParams( params: {'tab': index == 0 ? 'books' : 'authors'}, );
- Then, allow
State
restoring from browser’s history:
@override void didUpdateWidget(DynamicURLPage oldWidget) { super.didUpdateWidget(oldWidget); final values = APSNavigator.of(context).currentConfig.values; tabIndex = (values['tab'] == 'books') ? 0 : 1; }
What is important to know:
- Don’t forget to include a
Key
on thePage
created by thePageBuilder
to everything works properly.
3. Return Data Example
Example Link: Return Data Example
Push a new route and wait the result:
final selectedOption = await APSNavigator.of(context).push( path: '/return_data_example', );
Pop returning the data:
APSNavigator.of(context).pop('Do!');
What is important to know:
- Data will only be returned once.
- In case of user navigate your app and back again using the browser’s history, the result will be returned at
didUpdateWidget
method asresult,
instead ofawait
call.
@override void didUpdateWidget(HomePage oldWidget) { super.didUpdateWidget(oldWidget); final params = APSNavigator.of(context).currentConfig.values; result = params['result'] as String; if (result != null) _showSnackBar(result!); }
4. Multi Push
Example Link: Multi Push Example
Push a list of the Pages at once:
APSNavigator.of(context).pushAll( // position: (default is at top) list: [ ApsPushParam(path: '/multi_push', params: {'number': 1}), ApsPushParam(path: '/multi_push', params: {'number': 2}), ApsPushParam(path: '/multi_push', params: {'number': 3}), ApsPushParam(path: '/multi_push', params: {'number': 4}), ], );
In the example above ApsPushParam(path: '/multi_push', params: {'number': 4}),
will be the new top.
What is important to know:
- You don’t necessarily have to add at the top; you can use the
position
param to add the routes at the middle of Route Stack. - Don’t forget to include a
Key
on thePage
created by thePageBuilder
to everything works properly.
5. Multi Remove
Example Link: Multi Remove Example
Remove all the Pages you want given a range:
APSNavigator.of(context).removeRange(start: 2, end: 5);
6. Internal (Nested) Navigators
Example Link: Internal Navigator Example
class InternalNavigator extends StatefulWidget { final String initialRoute; const InternalNavigator({Key? key, required this.initialRoute}) : super(key: key); @override _InternalNavigatorState createState() => _InternalNavigatorState(); } class _InternalNavigatorState extends State<InternalNavigator> { late APSNavigator childNavigator = APSNavigator.from( parentNavigator: navigator, initialRoute: widget.initialRoute, initialParams: {'number': 1}, routes: { '/tab1': Tab1Page.route, '/tab2': Tab2Page.route, }, ); @override void didChangeDependencies() { super.didChangeDependencies(); childNavigator.interceptBackButton(context); } @override Widget build(BuildContext context) { return Router( routerDelegate: childNavigator, backButtonDispatcher: childNavigator.backButtonDispatcher, ); } }
What is important to know:
- Current limitation: Browser’s URL won’t update based on internal navigator state
Warning & Suggestions
- Although this package is already useful, it’s still in the Dev stage.
- I’m not sure if creating yet another navigating library is something good – we already have a lot of confusion around it today.
- This lib is not back-compatible with the old official Navigation API – at least for now (Is it worth it?).
- Do you have any ideas or found a bug? Fell free to open an issue! 🙂
- Do you want to know the current development stage? Check the Project’s Roadmap.
Download APS Navigator package source code on GitHub
Provides the list of the opensource Flutter apps collection with GitHub repository.