Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group Group

Android & Kotlin Tutorials 

The highest quality Android tutorials on the web - 100% in Kotlin!

Flutter Navigation Tutorial

Learn about routes, navigation, and transitions for apps written using the Flutter cross-platform framework from Google.

Version

  • Other, Android 4.4, Android Studio 3

What’s better than an app with one screen? Why, an app with two screens, of course! :]

Navigation is a key part of the UX for any mobile application. Due to the limited screen real estate on mobile devices, users will constantly be navigating between different screens, for example, from a list to a detail screen, from a shopping cart to a checkout screen, from a menu into a form, and many other cases. Good navigation helps your users find their way around and get a sense of the breadth of your app.

The iOS navigation experience is often built-around a UINavigationController, which uses a stack-based approach to shifting between screens. On Android, the Activity stack is the central means to shift a user between different screens. Unique transitions between screens in these stacks can help give your app a unique feel.

Just like the native SDKs, cross-platform development frameworks must provide a means for your app to switch between screens. In most cases, you’ll want the navigation approach to be consistent with what the users of each platform have come to expect.

Flutter is a cross-platform development SDK from Google that allows you to quickly create apps that target both iOS and Android from a single code base. If you’re new to Flutter, please checkout our Getting Started with Flutter tutorial to see the basics of working with Flutter.

In this tutorial, you’ll see how Flutter implements navigation between the different screens of a cross-platform app, by learning about:

  • Routes and navigation
  • Popping off the stack
  • Returning a value from a route
  • Custom navigation transitions

Getting Started

You can download the starter project for this tutorial from the materials link at the top or bottom of the page.

This tutorial will be using VS Code with the Flutter extension installed. You can also use IntelliJ IDEA or Android Studio, or work with a text editor of your choice with Flutter at the command line.

Open the starter project in VS Code by choosing File > Open and finding the root folder of the starter project zip file:

Open starter project

You’ll be prompted by VS Code to fetch the packages needed for the project, so go ahead and do so:

Fetch packages

Once the project is open in VS Code, hit F5 to build and run the starter project. If VS Code prompts you to choose an environment to run the app in, choose “Dart & Flutter”:

Choose environment

Here is the project running in the iOS Simulator:

Starter on iOS

And here it is running on an Android emulator:

Starter on Android

The “slow mode” banner you see is due to the fact that you’re running a debug build of the app.

The starter app shows the list of members in a GitHub organization. In this tutorial, we’ll navigate from this first screen to a new screen for each member.

Second Screen

We first need to create a screen to navigate to for each member. Elements of a Flutter UI take the form of UI widgets, so we’ll create a member widget.

First, right-click on the lib folder in the project, choose New File, and create a new file named memberwidget.dart:

New File

Add import statements and a StatefulWidget subclass named MemberWidget to the new file:

import 'package:flutter/material.dart';

import 'member.dart';


class MemberWidget extends StatefulWidget {
  final Member member;

  MemberWidget(this.member)  {
    if (member == null) {
      throw new ArgumentError("member of MemberWidget cannot be null. "
          "Received: '$member'");
    }
  }

  @override
  createState() => new MemberState(member);
}

A MemberWidget uses a MemberState class for its state, and passes along a Member object to the MemberState. You’ve made sure that the member argument is not-null in the widget constructor.

Add the MemberState class above MemberWidget in the same file:

class MemberState extends State<MemberWidget> {
  final Member member;

  MemberState(this.member);
}

Here, you’ve given MemberState a Member property and a constructor.

Each widget must override the build() method, so add the override to MemberState now:

@override
Widget build(BuildContext context) {
  return new Scaffold (
    appBar: new AppBar(
      title: new Text(member.login),
    ),
    body: new Padding(
      padding: new EdgeInsets.all(16.0),
      child: new Image.network(member.avatarUrl)
    )
  );
}

You’re creating a Scaffold, a material design container, which holds an AppBar and a Padding widget with a child Image for the member avatar.

With the member screen all setup, you now have somewhere to navigate to! :]

Routes

Navigation in Flutter is centered upon the idea of routes.

Routes are similar in concept to the routes that would be used in a REST API, where each route is relative to some root. The widget created by the main() method in you app acts like the root.

One way to use routes is with the PageRoute class. Since you’re working with a Flutter MaterialApp, you’ll use the MaterialPageRoute subclass.

Add an import to the top of GHFlutterState to pull in the member widget:

import 'memberwidget.dart';

Next add a private method _pushMember() to GHFlutterState in the file ghflutterwidget.dart:

_pushMember(Member member) {
  Navigator.of(context).push(
    new MaterialPageRoute(
      builder: (context) => new MemberWidget(member)
    )
  );
}

You’re using Navigator to push a new MaterialPageRoute onto the stack, and the MaterialPageRoute is built using your new MemberWidget.

Now you need to call _pushMember() when a user taps on a row in the list of members. You can do so by updating the _buildRow() method in GHFlutterState and adding an onTap attribute to the ListTile:

Widget _buildRow(int i) {
  return new Padding(
    padding: const EdgeInsets.all(16.0),
    child: new ListTile(
      title: new Text("${_members[i].login}", style: _biggerFont),
      leading: new CircleAvatar(
          backgroundColor: Colors.green,
          backgroundImage: new NetworkImage(_members[i].avatarUrl)
      ),
      // Add onTap here:
      onTap: () { _pushMember(_members[i]); },
    )
  );
}

When a row is tapped, your new method _pushMember() is called with the member that was tapped.

Hit F5 to build and run the app. Tap a member row and you should see the member detail screen come up:

Member screen Android

And here’s the member screen running on iOS:

Member screen iOS

Notice that the back button on Android has the Android style and the back button on iOS has the iOS style, and also that the transition style when switching to the new screen matches the platform transition style.

Tapping the back button takes you back to the member list, but what if you want to manually trigger going back from your own button in the app?

Take me back

Popping the stack

Since navigation in the Flutter app is working like a stack, and you’ve pushed a new screen widget onto the stack, you’ll pop from the stack in order to go back.

Add an IconButton to MemberState by updating its build() override to add a Column widget in place of just the Image:

@override
Widget build(BuildContext context) {
  return new Scaffold (
    appBar: new AppBar(
      title: new Text(member.login),
    ),
    body: new Padding(
      padding: new EdgeInsets.all(16.0),
      // Add Column here:
      child: new Column(
        children: [
          new Image.network(member.avatarUrl),
          new IconButton(
            icon: new Icon(Icons.arrow_back, color: Colors.green, size: 48.0),
            onPressed: () { Navigator.pop(context); }
            ),
        ]),
    )
  );
}

You’ve added the Column in order to layout the Image and an IconButton vertically. For the IconButton, you’ve set its onPressed value to call Navigator and pop the stack.

Build and run the app using F5, and you’ll be able to go back to the member list by tapping your new back arrow:

Popping the stack

Returning a value

Routes can return values, similar to results obtained in Android using onActivityResult().

To see a simple example, add the following private async method to MemberState:

_showOKScreen(BuildContext context) async {
  // 1, 2
  bool value = await Navigator.of(context).push(new MaterialPageRoute<bool>(
    builder: (BuildContext context) {
      return new Padding(
        padding: const EdgeInsets.all(32.0),
        // 3
        child: new Column(
        children: [
          new GestureDetector(
            child: new Text('OK'),
            // 4, 5
            onTap: () { Navigator.of(context).pop(true); }
          ),
          new GestureDetector(
            child: new Text('NOT OK'),
            // 4, 5
            onTap: () { Navigator.of(context).pop(false); }
          )
        ])
      );
    }
  ));
  // 6
  var alert = new AlertDialog(
    content: new Text((value != null && value) ? "OK was pressed" : "NOT OK or BACK was pressed"),
    actions: <Widget>[
      new FlatButton(
        child: new Text('OK'),
        // 7
        onPressed: () { Navigator.of(context).pop(); }
        )
    ],
  );
  // 8
  showDialog(context: context, child: alert);
}

Here is what’s going on in this method:

  1. You push a new MaterialPageRoute onto the stack, this time with a type parameter of bool.
  2. You use await when pushing the new route, which waits until the route is popped.
  3. The route you push onto the stack has a Column that shows two text widgets with gesture detectors.
  4. Tapping on the text widgets causes calls to Navigator to pop the new route off the stack.
  5. In the calls to pop(), you pass a return value of true if the user tapped the “OK” text on the screen, and false if the user tapped “NOT OK”. If the user presses the back button instead, the value returned is null.
  6. You then create an AlertDialog to show the result returned from the route.
  7. Note that the AlertDialog itself must be popped off the stack.
  8. You call showDialog() to show the alert.

The primary points to note in the above are the bool type parameter in MaterialPageRoute<bool>, which you would replace with any other type you want coming back from the route, and the fact that you pass the result back in the call to pop, for example, Navigator.of(context).pop(true).

Update build() in MemberState to have a RaisedButton that calls _showOKScreen():

@override
Widget build(BuildContext context) {
  return new Scaffold (
    appBar: new AppBar(
      title: new Text(member.login),
    ),
    body: new Padding(
      padding: new EdgeInsets.all(16.0),
      child: new Column(
        children: [
          new Image.network(member.avatarUrl),
          new IconButton(
            icon: new Icon(Icons.arrow_back, color: Colors.green, size: 48.0),
            onPressed: () { Navigator.pop(context); }
            ),
          // Add RaisedButton here:
          new RaisedButton(
            child: new Text('PRESS ME'),
            onPressed: () { _showOKScreen(context); }
            )
        ]),
    )
  );
}

The RaisedButton you’ve added shows the new screen.

Hit F5 to build and run the app, tap the “PRESS ME” button, and then tap either “OK”, “NOT OK”, or the back button. You’ll get a result back from the new screen showing which of the results the user tapped:

Route result

Custom Transitions

In order to give the navigation of your app a unique feel, you can create a custom transition. You can either extends classes such as PageRoute, or use a class like PageRouteBuilder that defines custom routes with callbacks.

Replace _pushMember in GHFlutterState so that it pushes a new PageRouteBuilder onto the stack:

_pushMember(Member member) {
  // 1
  Navigator.of(context).push(new PageRouteBuilder(
    opaque: true,
    // 2
    transitionDuration: const Duration(milliseconds: 1000),
    // 3
    pageBuilder: (BuildContext context, _, __) {
      return new MemberWidget(member);
    },
    // 4
    transitionsBuilder: (_, Animation<double> animation, __, Widget child) {
      return new FadeTransition(
        opacity: animation,
        child: new RotationTransition(
          turns: new Tween<double>(begin: 0.0, end: 1.0).animate(animation),
          child: child,
        ),
      );
    }
  ));
}

Here you:

  1. Push a new PageRouteBuilder onto the stack.
  2. Specify the duration using transitionDuration.
  3. Create the MemberWidget screen using pageBuilder.
  4. Use the transitionsBuilder attribute to create fade and rotation transitions when showing the new route.

Hit F5 to build and run the app, and see your new transition in action:

Custom transition

Wow! That’s making me a little dizzy! :]

Where to go from here?

You can download the completed project using the download button at the top or bottom of this tutorial.

You can learn more about Flutter navigation by visiting:

As you’re reading the docs, check out in particular how to make named routes, which you call on Navigator using pushNamed().

Stay tuned for more Flutter tutorials and screencasts!

Feel free to share your feedback, findings or ask any questions in the comments below or in the forums. I hoped you enjoyed learning about navigation with Flutter!

Contributors

Comments

Create your free learning account today!

With a free raywenderlich.com account, you can download source code from our tutorials, track your progress, personalize your learner profile, participate in open discussion forums and more!