Google’s UI toolkit, Flutter has caught the attention of a lot of developers, business owners etc. App development can be made much faster using Flutter and it also offers expressive and flexible UI. In a previous article, I shared my insights on using Flutter for mobile app development.
Navigation between widgets/screens is very important for all mobile applications. In this article, I’ll explain in step by step how to implement navigation between widgets and screens in Flutter and pass data between Widgets/Screens. The completely open-source SDK of Flutter is based on the Dart programming language, which is new, but easy to learn and get started with.
Navigator.push(context, Route<T>)
method to navigate to Widget/Screen.Navigator.pop(context)
method to navigate back to the previous Widget/Screen in the stack.initialRoute
and routes
properties in MaterialApp
widget.Navigator.pushNamed(context,routeName)
method to navigate between Widgets using named routes.pushNamed()
and pop()
method in NavigatorCreate two stateless widgets under lib/screens folder, one is called home_screen.dart and another one is profile_screen.dart.
In home_screen.dart, add a FlatButton
widget as follows to navigate to profile screen.
import 'package:flutter/material.dart'; class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Home Screen'),), body: Container( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Home Screen Content'), FlatButton( onPressed: () {}, child: Text('Navigate to Profile Screen', style: TextStyle(color: Colors.white)), color: Theme.of(context).primaryColor, ) ],), ), ); } }
Let's work together!
In profile_screen.dart, create a FlatButton
as follows for navigating back to home screen.
import 'package:flutter/material.dart'; class ProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Profile Screen'),), body: Container( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Profile Screen Content'), FlatButton( onPressed: () => backToHome(context), child: Text('Back to Home!', style: TextStyle(color: Colors.white)), color: Theme.of(context).primaryColor ) ],), ), ); } }
In the home screen button, create a method called navigateToProfile()
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import './profile_screen.dart'; class HomeScreen extends StatelessWidget { void navigateToProfile(BuildContext context) { Navigator.push(context, MaterialPageRoute(builder: (_) => ProfileScreen()) ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Home Screen'),), body: Container( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Home Screen Content'), FlatButton( onPressed: () => navigateToProfile(context), child: Text('Navigate to Profile Screen', style: TextStyle(color: Colors.white)), color: Theme.of(context).primaryColor, ) ],), ), ); } }
In order to navigate between widgets/screens, Flutter offers Navigator
class which has many methods.
Inside the anonymous function of the onPressed
, we are invoking the navigateToProfile(BuildContext context)
. We pass the context
from our build method because we need to use it for the flutter Navigator.push()
method.
To navigate to the profile screen, we should use the push
method of the Navigator
, the push method needs a context
as a first argument and Route<T>
as the second argument. In our case, we can use built-in MaterialRoute()
which flutter offers.
The MaterialRoute(builder: (ctx) => widget())
has a property called builder which accepts a function with a context parameter and returns a Widget. In our case, we need to navigate to Profile Screen. To do that, we are invoking the ProfileScreen()
widget inside the builder anonymous method
Navigator.push( context, MaterialPageRoute(builder: (_) => ProfileScreen()) );
As you see in the preview, that the AppBar
itself has a back button for navigating back to the previous screen. But anyway, we may face a situation where we need to create a button inside the body of the scaffold and navigate back to previous screens.
To navigate back, we can use the pop
method of the Navigator
. The pop()
method removes the current widget which was pushed using the push()
method, which in turn moves back to the previous screen.
In other words, the navigation in flutter works like a stack. If we push a widget, It will be placed on top of the other screens. If we invoke the pop()
method, then the screen at the top of the stack will be removed that results in navigating back to the previous screen.
Let’s create a backToHome
method in Profile Screen Widget to handle the Navigator.of(context).pop()
as follows
import 'package:flutter/material.dart'; class ProfileScreen extends StatelessWidget { void backToHome(context) { Navigator.pop(context); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Profile Screen'),), body: Container( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Profile Screen Content'), FlatButton( onPressed: () => backToHome(context), child: Text('Back to Home!', style: TextStyle(color: Colors.white)), color: Theme.of(context).primaryColor ) ],), ), ); } }
For a large application, creating a MaterialRoute
on the fly for navigation is not an ideal solution. We need to maintain all the routes in one place, to solve that, flutter offers named routing.
The MaterialApp
has a way to define routes for our application. The initialRoute
property is used to define the default page or landing page of our application. The routes
property is a Map with String as Key and Function which returns Widget as it’s value.
Before defining our routes in the material app, we need to add the route name for our Widgets/Screens. We can hard code the route names in the routes argument but that’s not the ideal way to do, because we need to hard code the route name in all the places where we need to navigate. So, the best practice is to use static property inside each Widget/Screen as follows.
Inside the home_screen.dart, define the routeName
property as follows:
static const routeName = '/';
In your profile_screen.dart, define the routeName
property as follows: (you can use any property name you want, I am using routeName
)
static const routeName = '/profile';
Add initialRoute
and routes
properties in MaterialApp
(main.dart), Don’t forget to remove home
property when we use initialRoute
for our application.
import 'package:flutter/material.dart'; import './screens/home_screen.dart'; import './screens/profile_screen.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), initialRoute: '/', routes: { HomeScreen.routeName: (ctx) => HomeScreen(), ProfileScreen.routeName: (ctx) => ProfileScreen() }, ); } }
The HomeScreen.routeName
and ProfileScreen.routeName
are the static properties that we have defined in our Widgets. These are mapped to anonymous function which returns our respective widgets.
To use named routing in our home screen widget, we can replace the push
method to pushNamed
method. The pushNamed
method, the first argument is the context
and the second one is the route name. Let’s modify our home screen widget.
void navigateToProfile(BuildContext context) { Navigator.pushNamed(context, ProfileScreen.routeName); }
As of now, we are using the stack method of flutter navigation. So to remove the old pages completely from the stack while navigating to the new page, we can replace the pushNamed
with pushReplacementNamed
. It will clear the current page from the stack and push the new page into the stack.
void navigateToProfile(BuildContext context) { Navigator.pushReplacementNamed(context, ProfileScreen.routeName); }
In the preview, you can clearly see that the back button in the app bar is no more provided by flutter. Because the stack has only one item that is the current widget. And also, you can’t use the pop()
method if there are no previous screens in the stack. If you press the ‘back to home’ button then you’ll see only a black blank screen.
To move to the previous screen properly, you need to use the pushReplacementNamed
method inside the ‘back to home’ button. but I am not going to do that now. As I have to show you how to pass data between routes in the next section.
In a large application, you may face a situation where you need to pass data between routes, that is from one widget to another widget.
The pushNamed
and pushReplacementNamed
have a property called arguments. We can assign any value to the argument and pass it to the navigating widget.
void navigateToProfile(BuildContext context) { Navigator.pushReplacementNamed(context, ProfileScreen.routeName, arguments: 'Some Text Data'); }
For brevity, I am sharing some text data to the Profile Screen Widget. In order to access the data in the profile screen widget, we need to use ModalRoute.of(context).settings.argument
inside the build method.
import 'package:flutter/material.dart'; class ProfileScreen extends StatelessWidget { static const routeName = '/profile'; void backToHome(context) { Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final textData = ModalRoute.of(context).settings.arguments as string; return Scaffold( appBar: AppBar(title: Text('Profile Screen'),), body: Container( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(textData), FlatButton( onPressed: () => backToHome(context), child: Text('Back to Home!', style: TextStyle(color: Colors.white)), color: Theme.of(context).primaryColor ) ],), ), ); } }
You can assign the value to a variable, and bind the data in the widget. I have passed the data to the Text Widget above the FlatButton
.
The Navigator.pop()
method has a second optional argument which is used to pass data to the previous screens when we navigate back. Let’s implement a small example in our profile screen and home screen.
In your profile screen, send a boolean value via pop()
method as follows
void backToHome(context) { Navigator.of(context).pop(true); }
In your home screen widget, use pushNamed
. The pushNamed
, push
and pushReplacementNamed
will return a Future. If you are from a JavaScript background, The Future is a Promise in Dart. You can use then
method which accepts a function as a parameter to receive data from the profile screen to the home screen.
Declare a property called showText
with a value false in your home screen widget. Add the then
method to the pushNamed and update the showText
value once it’s resolved.
bool showText = false; void navigateToProfile(BuildContext context) { Navigator.pushNamed(context, ProfileScreen.routeName, arguments: 'Some Text Data').then((val) { if(val) { showText = val; } }); }
Let's work together!
Based on the showText
value, we can show a different Text Widget in our home screen component. Just to test that it’s working. So our final home screen widget code is:
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import './profile_screen.dart'; class HomeScreen extends StatelessWidget { static const routeName = '/'; bool showText = false; void navigateToProfile(BuildContext context) { Navigator.pushNamed(context, ProfileScreen.routeName, arguments: 'Some Text Data').then((val) { if(val) { showText = val; } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Home Screen'),), body: Container( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ showText ? Text('Data Got From Profile Widget') : Text('Home Screen Content'), FlatButton( onPressed: () => navigateToProfile(context), child: Text('Navigate to Profile Screen', style: TextStyle(color: Colors.white)), color: Theme.of(context).primaryColor, ) ],), ), ); } }
Your profile screen widget code is:
import 'package:flutter/material.dart'; class ProfileScreen extends StatelessWidget { static const routeName = '/profile'; void backToHome(context) { Navigator.of(context).pop(true); } @override Widget build(BuildContext context) { final textData = ModalRoute.of(context).settings.arguments as String; return Scaffold( appBar: AppBar(title: Text('Profile Screen'),), body: Container( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(textData), FlatButton( onPressed: () => backToHome(context), child: Text('Back to Home!', style: TextStyle(color: Colors.white)), color: Theme.of(context).primaryColor ) ],), ), ); } }
Your main.dart code is:
import 'package:flutter/material.dart'; import './screens/home_screen.dart'; import './screens/profile_screen.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), initialRoute: '/', routes: { HomeScreen.routeName: (ctx) => HomeScreen(), ProfileScreen.routeName: (ctx) => ProfileScreen() }, ); } }
I hope, you have learned how to navigate between widgets in flutter and also how to share data back and forth using ModalRoute and pop method in flutter.