A simple and easy to learn declarative navigation framework for Flutter, based on Navigator 2.0 (Router).
If you love Flutter, you would love declarative UI and therefore declarative navigation.
Navigator 2.0 provides a declarative navigation API. Unfortunately, it's too complex and difficult to use with a lot of boilerplate. Not only that, it requires to keep a single state to manage the whole navigation system of your application. It's not a good architecture, and definitely does not fit in large scale applications.
Navi helps you keep all the powerful of Navigator 2.0 but with a simple and easy to learn API. It helps you manage your navigation system in split and isolated domains.
Note that, imperative navigation API is also supported as an extra layer beyond the declarative API.
- Quick example
- More examples
- Architecture layers
- Declarative navigation
- Navigate to a new route
- Nested stack
- Milestones
- Contributing to Navi
Quick example
To use the library, controlling NaviStack
widget is everything you need to learn!
Below is an app with 2 pages:
/
shows list of books/:id
shows a book.
void main() {
runApp(App());
}
class App extends StatelessWidget {
final _informationParser = NaviInformationParser();
final _routerDelegate = NaviRouterDelegate.material(child: BooksStack());
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationParser: _informationParser,
routerDelegate: _routerDelegate,
);
}
}
// your BooksStack widget state should use NaviRouteMixin in order to receive notification on route change
class _BooksStackState extends State<BooksStack> with NaviRouteMixin<BooksStack> {
Book? _selectedBook;
@override
void onNewRoute(NaviRoute unprocessedRoute) {
// if route changes (ex. browser address bar or deeplink), convert route to your state and rebuild the widget (stack)
final bookId = int.tryParse(unprocessedRoute.pathSegmentAt(0) ?? '');
_selectedBook = getBookById(bookId); // ex. get from database
setState(() {});
}
@override
Widget build(BuildContext context) {
// you can nest NaviStack under another NaviStack without limitation to create nested routes
// see 'Nested stack' secion below
return NaviStack(
pages: (context) => [
NaviPage.material(
key: const ValueKey('Books'),
// without route property, url is '/' by default
// BooksPagelet is your widget, which shows list of books
child: BooksPagelet(
// you can update state of BooksStack widget to navigate
// or you can use context.navi to navigate inside BooksPagelet (see 'Navigate to a new route' section below)
onSelectBook: (book) => setState(() {
_selectedBook = book;
}),
),
),
if (_selectedBook != null)
NaviPage.material(
key: ValueKey(_selectedBook),
route: NaviRoute(path: ['${_selectedBook!.id}']), // url is '/:id'
// BookPagelet is your widget, which shows a book
child: BookPagelet(book: _selectedBook!),
),
],
onPopPage: (context, route, dynamic result) {
// update state when pop
if (_selectedBook != null) {
setState(() {
_selectedBook = null;
});
}
},
);
}
}
More examples
- Deep Linking - Path Parameters
- Deep Linking - Query Parameters
- Login/Logout/Sign-up Routing
- Skipping Stacks
- Dynamic Linking
- Nested Routing - Bottom Navigation Bar - without Keeping State
- Nested Routing - Bottom Navigation Bar - Keeping State
- Nested Routing - Tab Bar - Without Keeping State
- Nested Routing - Tab Bar - Keeping State
Architecture layers
Packages | Layers | Plan | Explanation |
---|---|---|---|
Navi | Code Generator | After release 1.0 | Generate boilerplate code |
Navi | Configurator | Before release 1.0 if possible, otherwise after release 1.0 | Comparable to URL mapping approaches like Angular or Vue |
Navi | Imperative API | Before release 1.0 | Useful when declarative is not needed |
Navi | High-level declarative API | WIP | Simple and easy to use yet keep the powerful of Navigator 2.0 |
Flutter SDK Navigator 2.0 | Low-level declarative API | N/A | Too complex and difficult to use |
Declarative navigation
Declarative navigation is similar to declarative UI as it allows you to describe your navigation system by the current UI state. Updating your current UI state to tell Navi figures out where to navigate to.
Using declarative navigation is only powerful if
- the provided API is simple enough
- your application is reasonable split into manageable domains (or stacks in this library)
An example, where declarative shines is to manage a chain of pages to complete a single task (ex. registration form with multiple pages). Using imperative approach is usually more difficult in this case.
Chain of pages scenario (also known as flow of pages) is just one case, you can use with Navi. This library is definitely much more than that.
Navigate to a new route
To navigate you have 2 options:
- Declarative: update your widget state (or multiple widget states) to rebuild the widgets. It will rebuild the needed stacks and update URL accordingly.
- Imperative: calling the methods below
context.navi.to(['products', '1'])
orcontext.navi.to(['products/1'])
: navigates to absolute URL/products/1
.context.navi.relativeTo(['details'])
: navigates to relative URL. If current URL (context.navi.currentRoute
) is/products/1
, the destination URL will be/products/1/details
. You can use../
to goes up one level in the route. For example,context.navi.relativeTo(['../2/details'])
will navigate to/products/2/details
in this example.context.navi.pop()
: shortcut ofNavigator.of(context).pop()
context.navi.maybePop()
: shortcut ofNavigator.of(context).maybePop()
context.navi.canPop()
: shortcut ofNavigator.of(context).canPop()
- TODOs:
context.navi.stack(ProductsStackMarker()).to(['2', 'overview'])
: navigates to relative URL starting from current URL of the given stack. If current URL ismy/long/path/to/products/1/details
andProductsStack
URL ismy/long/path/to/products
, the destination URL will bemy/path/to/products/2/overview
.context.navi.back()
: moves back to the previous page in the history.
Nested stack
Because NaviStack
is just a normal widget, you only need to use this widget to build nested stacks like you would do
with other widgets.
For example, you have a bookstore with 2 pages: book list page and book page. Their URLs are /books
and /books/:id
.
In book page, you split the content into 2 tabs: overview and details. Their URLs are /books/:id/overview
, /books/:id/details
.
In this case, you can create 2 stacks:
// This stack could be your RootStack, maybe directly under your MaterialApp.
NaviStack(
pages: (context) => [
NaviPage.material(
route: NaviRoute(path: ['books']),
child: BooksPagelet(),
),
NaviPage.material(
route: NaviRoute(path: ['books', book.id]),
child: BookStack(),
),
],
);
// In BookStack widget, you build another stack
NaviStack(
pages: (context) => [
if (pageId == 'overview') NaviPage.material(
route: NaviRoute(path: ['overview']),
child: BookOverviewPagelet(),
),
if (pageId == 'details') NaviPage.material(
route: NaviRoute(path: ['details']),
child: BookDetailsPagelet(),
),
],
);
The main idea is that, in the nested stack BookStack
, you don't need to know the URL of parent stack (RootStack
in
this case).
Navi will help you merge the current URL in parent stack (ex. /books/1
) and nested stack (ex. /overview
) to generate
the final URL for you (ex. /books/1/overview
).
You can have unlimited nested stacks as deep as you want and each stack manage only the URL part it should know.
It's commonly used together
with BottomNavigationBar
and TabBar
, but it will definitely work with other
components and designs.
If you want to keep state of nested stacks in BottomNavigationBar
, you could
use IndexedStack
.
If you want to keep state of nested stacks in TabBar
, you could
use AutomaticKeepAliveClientMixin
.
When use with tabs and keeping state of tabs, you keep multiple stack branches in the widget tree. In this case, please
make sure to set active: false
to inactive stacks in inactive tabs. Only the stack in the current tab
set active: true
. The active stack is responsible to report the correct final URL to browser address bar, while
inactive stacks don't.
Please see more in Examples.
Custom page
To use default material and cupertino pages, you can use shortcuts:
NaviRouterDelegate.material()
NaviRouterDelegate.cupertino()
NaviPage.material()
NaviPage.cupertino()
To use a custom page, use the default constructors:
NaviRouterDelegate(rootPage: () => YourCustomPage())
NaviPage(pageBuilder: (key, child) => YourCustomPage())
TODO: Flatten list of stacks to a single stack
FlatNaviStack(
children: [
NaviStack(),
NaviStack(),
// ...
]
)
FlatNaviStack
merges all pages of child stacks into a single stack.
The difference is that, URL of nested stacks are dependent, but URLs of stacks in FlatNaviStack
are independent.
TODO: Manipulation of the chronological history Stack
Browser back button and system back button should behave the same way, according to material navigation guideline
Milestones
The goal of Navi package is to create a friendly declarative navigation API for Flutter projects. It depends heavily on Navigator 2.0.
- Milestone 1 (WIP)
- Easy to learn, simple to maintain and organize application code based on split domains.
- Easy to migrate from non-web app to web app.
- Keep boilerplate code at reasonable level. More optimization will be in next milestones.
- Flexible: easily integrate with other architectural elements, especially, state management (ex. Bloc) and dependency injection (ex. get_it) .
- Modularization
- friendly to projects, which require splitting into multiple teams
- each stack can be considered as an isolated module
- Imperative navigation API is also supported.
- Milestone 2 (Plan: before release 1.0)
- Optimize to remove more boilerplate code for common/general scenarios
- Optimize performance
- Test coverage at least 90%
- Evaluate edge cases
- Milestone 3 (Plan: before release 1.0 if possible, otherwise after release 1.0)
- Implement a configurator, which fits to common scenarios to remove more boilerplate code. For more flexibility, use the high level declarative API.
- Milestone 4 (Plan: after release 1.0)
- Implement code generator to even remove more boilerplate code
Contributing to Navi
First of all, thank you a lot to visit Navi project!
Everyone is welcome to
- file issues on GitHub
- help people asking for help
- click the GitHub star/watch button
- click the Pub.dev like button
- contribute code via pull requests
The more people interested in the project, the more motivation I will have to speed up the development.
Enjoy to use Navi!