Flutter Getting Started: Tutorial 3 Navigation
Flutter Navigation, Let's move among pages
Introduction
In this article, I would discuss Flutter Navigation, Navigation or movement among pages is important business requirement of any application, for example almost every application requires a login page or option page, where various options could be set based on user input.
In simpler terms, it means leaving one page and loading a new page. Based on code, page may persist in memory or be simply deleted from stack. So at most, care should be taken by Developer/Architect while developing an application, what should be kept in memory and what shouldn't.
Background
If you happen to read my article, Flutter Getting Started: Tutorial 2 - StateFulWidget, we will build on the same application. Let me summarize what I have done in that article - I have provided a TextField
which is similar to TextBox
in C#, and on press of Save button, it gets added to list which also presents the same page.
Now, we destroy that application and create two pages out of it, one which will display the list of cities added and the other page provides options for adding the cities. So this would be the final result once the application is completed.
Add City new UI design |
![]() |
Here, as mentioned above, form portion going into |
Aim of the Tutorial
As mentioned earlier in this article, I will be building an application based on my previous article. I will be creating two pages application and in the end, I will be adding additional pages to showcase removing of multiple pages.
- There would be two pages:
- Page #1:
Homepage
would containListView
, where all the user input city would be displayed - Page #2:
AddDataPage
would containTextField
from where you can add data to list (it containsTextField, Cancel
andAdd RaisedButton
)
- Page #1:
- Task #1: Basic Navigation, when we navigate between pages using known page object
- Task #2: Sending data using
Navigation
object - Task #3: Using named route for navigation instead of page object
- Bonus Task#4: Additional page would be added to show navigation stack, and how pages persist between navigation
Overall, application would contain two pages, one would show all the text entered by user and the other page would provide form to enter data.
Using the Code
- Let's start by creating a new flutter project in Android Studio, Give it name
Flutter3_navigation
. In case you don't know how to create Flutter project, please read here. - Create a package in lib directory by name pages and add two DART files:
adddatapage.dart
: AddAddDataPage
class which extendStatelesswidget
homepage.dart
: AddHomePage
class which extendStateFulWidget
(if you don't know aboutStateFulWidget
, read here), also add_HomePageState
which extendsstate<HomePage>
- Delete all code from
main.dart
, the entry point of Mobile Application, and replace with this code:import 'package:flutter/material.dart'; import 'package:flutter3_navigation/pages/homepage.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue ), home: new HomePage(title: 'Flutter 3 : Navigation'), ); } }
Explanation of Code
- Here,
MyApp
is our entry class for mobile application, here we returnMaterialApp
object. This will provide skeleton for our app. - I set home object to
HomePage
object, which means it would be loaded when application starts running.
- Here,
- Put the following code in
HomePage.dart
, the following control has been added:import 'package:flutter/material.dart'; import 'package:flutter3_navigation/pages/adddatapage.dart'; class HomePage extends StatefulWidget { final String title; HomePage({Key key, this.title}) : super(key: key); @override _HomePageState createState() => new _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text( widget.title, style: new TextStyle(fontSize: 16.0), ), actions: <Widget>[ new IconButton( icon: new Icon(Icons.add), onPressed: () { print("Add button clicked"); }) ], ), ); }}
- Here, we create page
Scaffold
and createAppBar
inside it. - Here is a new addition in App Bar, I have added
IconButton
with display ofIcons.add
. This is a placeholder for navigating toAddDataPage
, currently we are not doing anything:
- Here, we create page
- Now, before I show you code for
AddDataPage.dart
, let me give you a small overview ofNavigator
Class. We generally use the following function (definition taken from Flutter website):Navigator.push
- Push the given route onto the navigator that most tightly encloses the given contextNavigator.pop
- Pop the top-most route off the navigator that most tightly encloses the given contextNavigator.pushNamed
- Push a named route onto the navigator that most tightly encloses the given context. We are going to use it in Task#3.Navigator.pushNamedAndRemoveUntil
- Push the route with the given name onto the navigator that most tightly encloses the given context, and then remove all the previous routes until thepredicate
returnstrue
. We are going to use in Task#4.
- Now, add function
void _onAddCityButtonPressed(BuildContext context)
in HomePage.dart file and call that fromAdd IconButton
:void _onAddCityButtonPressed(BuildContext context) { Navigator.push( context, MaterialPageRoute(builder: (context) => AddDataPage()) ) }
Explanation of Code
- As mentioned in step 5, I am using
Navigator.Push
to push new page on stack, here we need to use currentBuildContext
and PushAddDataPage
, when person clicks on+ button
inAppBar
.
- As mentioned in step 5, I am using
- Now it's the turn of AddDataPage.dart. Add the following code:
import 'package:flutter/material.dart'; class AddDataPage extends StatelessWidget { final TextEditingController _CityNameController = new TextEditingController(); @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text( "Add City", style: new TextStyle(fontSize: 16.0), ), ), body: new SafeArea( child: new Container( child: new Column(children: <Widget>[ new TextField( decoration: new InputDecoration( labelText: "City Name", ), controller: _CityNameController, ), new ButtonBar( alignment: MainAxisAlignment.end, children: <Widget>[ new RaisedButton( onPressed: () { Navigator.pop(context); }, child: new Text("Cancel"), ), new RaisedButton( onPressed: () { Navigator.pop(context,_CityNameController.text); }, child: new Text("Add"), ) ], ) ]), ), )); } }
Explanation of Code
- Here in this page, I have added a
TextField
, aButtonBar
with two buttonCancel
(which will pop the current page) andAdd
(which will return whatever data is entered inTextField
). - For returning data when unloading page, you can use the second parameter with whatever data you want to return. In our case,
CityName
.
- Here in this page, I have added a
- Now we are sending the data, but we haven't written code to catch whatever is coming from the other page, the answer lies in
Future
class, its type of object which keeps track of object coming in future and lets you catch its linked function. I would be writing a full-fledged article using async function and Future
in Dart language. It is similar toObservable/ Promises
inAngular
. Here is how we going to catchCityName
sent byAddDataPage
toHomePage
. Here is a modified function that looks like:void _onAddCityButtonPressed(BuildContext context) { Navigator.push( context, MaterialPageRoute(builder: (context) => AddDataPage()) ) .then((Object result){ print("result from addpage ${result.toString()}"); }); }
Explanation of Code
then
function is part ofFuture
class, this function will be anchor to page you navigate to, and bring you data whatever sent from there.- There are various ways to achieve this (gathering of data), I am showing only one way here, as it's not fruitful to tell them before letting you know how
async
works in DART.
- Now I modified code to include
ListView
in theHomePage
and adding data received fromAddDataPage
, all explanation of the below code is available in my previous article here.final List<Text> _lstCities = new List<Text>(); Widget _getListViewFromBuilder() { return ListView.builder( itemCount: _lstCities.length, shrinkWrap: true, padding: const EdgeInsets.all(16.0), itemBuilder: getListItems, ); } // Call back function, will called for each item in the Widget getListItems(BuildContext context, int index) { return _lstCities[index]; } // this function would called when add city button pressed void _onAddCityButtonPressed(BuildContext context) { Navigator .push(context, MaterialPageRoute(builder: (context) => AddDataPage())) .then((Object result) { if (result == null) return; setState(() { _lstCities.add(new Text( "${_lstCities.length + 1} ${result.toString()}", textAlign: TextAlign.justify, style: new TextStyle(fontWeight: FontWeight.bold,fontSize: 24.0), )); }); }); }
Here, we come to end of Task#2.
- Now the problem with the above code, the routing is fixed, i.e., since we are navigating through providing new object for page we want to load, this creates a problem if we want to have dynamic routes and avoid references of pages in a different project (you need to
import
class file, before providing page object). - Now you have to modify the
Main.dart
with the following changes,home
property needs to be commented out, as we are relying completely on routing now://home: new HomePage(title: 'Flutter 3 : Navigation'), routes: { '/' : (BuildContext build)=> new HomePage(title: 'Flutter 3 : Navigation'), '/adddata':(BuildContext build)=> new AddDataPage(), }, initialRoute: "/"
Explanation of Code
- Here, I commented out
home
property, as we are no longer using it. - Add
route
property, which is map ofString
as key andWidgetBuilder
as value, as going forward, we needstring
key to identify route we need to create. - Now instead of
Navigator.push
inHomePage
, useNavigator.pushNamed
:/* Task # 1 and 2 Navigator .push(context, MaterialPageRoute(builder: (context) => AddDataPage())) */ /* Task 3 and 4 */ Navigator.pushNamed(context,'/adddata' ) .then((Object result) {
- In the above, since we want to navigate to
AddDataPage
, we use itsString
key to navigate.
- Here, I commented out
- Now for the bonus part, here we would be adding a new page by name
EndPage
and I would include bottom bar in bothAddDataPage
andEndPage
.AddDataPage bottom bar
would contain forward navigation to end, which will useNavigator.pushNamed
to reachEndPage
EndPage
bottom bar would have one home button, which navigates to home page usingpushNamedAndRemoveUntil
and backward button to moveHomePage
- Now modified
AddDataPage
looks like, inside page container addbottomNavigationBar
and new function_getBottomNavigationBarWidget()
:bottomNavigationBar: _getbottomNavigationBarWidget(context),
New function needs to be added at the bottom of class, don't forget to route definition for
EndPage
inmain.dart
, as mentioned in Step 11.Widget _getbottomNavigationBarWidget(BuildContext context) { return Container( color: Colors.black87, child: new ButtonBar( mainAxisSize: MainAxisSize.max, alignment: MainAxisAlignment.end, children: <Widget>[ new IconButton(icon: new Icon (Icons.arrow_forward,size: 24.0,color: Colors.white,), onPressed: (){ Navigator.pushNamed(context, "/endpage"); }) ], ), ); }
Here, we added an
IconButton
inbottombar
, on pressing the button, we navigate toendpage
: - Add a new dart file in pages folder by
endpage.dart
, and addEndPage
class to it, since we are not managing any state inside it, we can safely derived it fromStatelessWidget
, code would look like this:import 'package:flutter/material.dart'; class EndPage extends StatelessWidget { @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text( "End Page", style: new TextStyle(fontSize: 16.0), ), ), body: new Center( child: new Text("Welcome to End Page", style: new TextStyle(fontSize: 24.0,fontWeight: FontWeight.bold),), ), bottomNavigationBar: _getbottomNavigationBarWidget(context), ); } Widget _getbottomNavigationBarWidget(BuildContext context) { return Container( color: Colors.black87, child: new ButtonBar( mainAxisSize: MainAxisSize.max, alignment: MainAxisAlignment.end, children: <Widget>[ new IconButton(icon: new Icon (Icons.arrow_back,size: 24.0,color: Colors.white,), onPressed: (){ Navigator.pop(context); }), new IconButton(icon: new Icon (Icons.home,size: 24.0,color: Colors.white,), onPressed: (){ Navigator.pushNamedAndRemoveUntil(context, "/", (Route<dynamic> route){ return false;}); }) ], ), ); } }
Here, we have two buttons in
BottomNavigationBar
, on pressingbackward
button, we pop current page and move toAddDataPage
. On pressing Home button, we remove all pages till we showHomePage
.Here, we come to the end of Task#4.
- Here is a GIF file of final output.
Though there are a lot of things left, like Page Animation during transition, etc. Hopefully, I have covered all basic maneuvering in this article. Please don't hesitate to ask any question or comment.
Points of Interest
Please go through these articles. It might give you a headway, where the wind is actually flowing:
- Flutter — 5 reasons why you may love it
- What’s Revolutionary about Flutter
- Why Flutter Uses Dart
- Github: https://github.com/thatsalok/FlutterExample/tree/master/flutter3_navigation
Flutter Tutorial
Dart Tutorial
History
- 12th July, 2018: First version