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
Comments are closed.