Navigation in Flutter

Summary

In this lesson we'll cover:

The Code for This Lesson

You can check out the step/step06 branch here: which will contain the code for this lesson.

Updating Our Fixture Data

In this lesson, we'll be showing a list of locations, so we'll need to add more to our list of fixture data.

// models/location.dart

class Location {
  final int id;

  // ...

  static List<Location> fetchAll() {
    return [
      Location(1, 'Kiyomizu-dera', 'assets/images/kiyomizu-dera.jpg', [
        // ...
      ]),
      Location(2, 'Mount Fuji', 'assets/images/fuji.jpg', [
        // ...
      ]),
      Location(3, 'Arashiyama Bamboo Grove', 'assets/images/arashiyama.jpg', [
        // ...
      ]),

    // ...

Implementing our Location Listing Screen

// locations/locations.dart

import 'package:flutter/material.dart';
import '../../app.dart';
import '../../models/location.dart';

class Locations extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // NOTE: we'll be moving this to a scoped_model later
    final locations = Location.fetchAll();

    return Scaffold(
      appBar: AppBar(
        title: Text('Locations'),
      ),
      body: ListView(
        children: locations
          .map((location) => Text(location.name))
          .toList(),
      ),
    );
  }

  // ...
}

WARNING: the above code snippet will not be our final implementation in this lesson.

Implementing a GestureDetector for each ListView Item

// locations/locations.dart

import 'package:flutter/material.dart';
import '../../app.dart';
import '../../models/location.dart';

class Locations extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // NOTE: we'll be moving this to a scoped_model later
    final locations = Location.fetchAll();

    return Scaffold(
      appBar: AppBar(
        title: Text('Locations'),
      ),
      body: ListView(
        children: locations
          .map((location) => GestureDetector(
              onTap: () => _onLocationTap(context, location.id),
              child: Container(child: Text(location.name))))
          .toList(),
      ),
    );
  }

  _onLocationTap(BuildContext context, int locationID) {
    // TODO later in this lesson, navigation!
    print('do something');
  }
}

Updating our LocationDetail to Take a Location ID Parameter

// location_detail/location_detail.dart

// ...

class LocationDetail extends StatelessWidget {
  final int _locationID;

  LocationDetail(this._locationID);

  @override
  Widget build(BuildContext context) {
    final location = Location.fetchByID(_locationID);

    // ...

// location_detail/location_detail.dart

import 'package:flutter/material.dart';
import '../../models/location.dart';
import 'image_banner.dart';
import 'text_section.dart';

class LocationDetail extends StatelessWidget {
  final int _locationID;

  LocationDetail(this._locationID);

  @override
  Widget build(BuildContext context) {
    // NOTE: we'll be moving this to a scoped_model later
    final location = Location.fetchByID(_locationID);

    return Scaffold(
      appBar: AppBar(
        title: Text(location.name),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          ImageBanner(location.imagePath),
        ]..addAll(textSections(location))),
    );
  }

  List<Widget> textSections(Location location) {
    return location.facts
      .map((fact) => TextSection(fact.title, fact.text))
      .toList();
  }
}

Navigator, MaterialPageRoute

Now let's talk about navigation. We'll implement the bit of logic that handles the tap event we added in the previous step now.

"Navigation" is simply the ability to move from screen to screen in a mobile or web app. A navigation "route" is effectively a name we define for where our screen lives, kind of like a URL.

So for example, if we've implemented a screen implemented as widget Screen1 we can navigate to it via:


Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => Screen1()),
);

NOTE this example was taken from

Using Named Routes


MaterialApp(
  // Start the app with the "/" named route. In our case, the app will start
  // on the FirstScreen Widget
  initialRoute: '/',
  routes: {
    // When we navigate to the "/" route, build the FirstScreen Widget
    '/': (context) => FirstScreen(),
    // When we navigate to the "/second" route, build the SecondScreen Widget
    '/second': (context) => SecondScreen(),
  },
);

We can then use the route and refer to it by name:


Navigator.pushNamed(context, '/second');

NOTE this example was taken from

Passing Arguments to Named Routes

// app.dart

// ...

MaterialApp(
  onGenerateRoute: _routes(),
);

// ...

RouteFactory _routes() {
  return (settings) {
    final Map<String, dynamic> arguments = settings.arguments;
    Widget screen;
    switch (settings.name) {
      case LocationsRoute:
        screen = Locations();
        break;
      case LocationDetailRoute:
        screen = LocationDetail(arguments['id']);
        break;
      default:
        return null;
    }
    return MaterialPageRoute(builder: (BuildContext context) => screen);
  };
}

Adding Our Navigation Event


// ...

_onLocationTap(BuildContext context, int locationID) {
  Navigator.pushNamed(context, LocationDetailRoute,
      arguments: {"id": locationID});
}

Here's the final locations.dart file:

// locations/locations.dart

import 'package:flutter/material.dart';
import '../../app.dart';
import '../../models/location.dart';

class Locations extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // NOTE: we'll be moving this to a scoped_model later
    final locations = Location.fetchAll();

    return Scaffold(
      appBar: AppBar(
        title: Text('Locations'),
      ),
      body: ListView(
        children: locations
            .map((location) => GestureDetector(
                onTap: () => _onLocationTap(context, location.id),
                child: Container(child: Text(location.name))))
            .toList(),
      ),
    );
  }

  _onLocationTap(BuildContext context, int locationID) {
    Navigator.pushNamed(context, LocationDetailRoute,
        arguments: {"id": locationID});
  }
}

Here's the final app.dart file:

// app.dart

import 'package:flutter/material.dart';
import 'screens/locations/locations.dart';
import 'screens/location_detail/location_detail.dart';
import 'style.dart';

const LocationsRoute = '/';
const LocationDetailRoute = '/location_detail';

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: _routes(),
      theme: _theme(),
    );
  }

  RouteFactory _routes() {
    return (settings) {
      final Map<String, dynamic> arguments = settings.arguments;
      Widget screen;
      switch (settings.name) {
        case LocationsRoute:
          screen = Locations();
          break;
        case LocationDetailRoute:
          screen = LocationDetail(arguments['id']);
          break;
        default:
          return null;
      }
      return MaterialPageRoute(builder: (BuildContext context) => screen);
    };
  }

  ThemeData _theme() {
    return ThemeData(
        appBarTheme: AppBarTheme(textTheme: TextTheme(title: AppBarTextStyle)),
        textTheme: TextTheme(
          title: TitleTextStyle,
          body1: Body1TextStyle,
        ));
  }
}

How to 'Go Back'

Summary

In this lesson we covered a realistic example of navigation as well as how to pass arguments to the screen we are navigating to. We've also avoided the official documentation's not-so-ideal "Pass Arguments to a Named Route" approach by using MaterialApp's onGenerateRoute.****