A wrapper around Navigator 2.0 and Router/Pages API
APS Navigator - App Pagination System
A wrapper around Navigator 2.0 and Router/Pages to make their use a easier.
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