Home Tutorials Training Consulting Books Company Contact us


Get more...

This tutorial explains how to navigate between build Flutter web applications.

1. Implementing Navigation in Flutter via Routing

Flutter can navigate between different screens. Screens in Flutter are widgets which cover the whole screen. Implementing different screens helps separating concerns, encourages encapsulation of code and thus makes the code base easier to read and maintain.

The navigation between screens (widgets) is handled by the Navigator class. It has different functions available that allow for either:

  • showing a screen directly

  • using named routes

To show a screen directly, you would provide it as a parameter to push method of Navigator.

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

If you are using named routes, you pass a map of all routes to the routes parameter of the MaterialApp constructor. The keys of the map should be a string with the name of the route (typically these start with a /, similar to HTTP paths). The value of each key is a function that takes the context of the app as an argument and returns an instance of the widget of the page.

This could look like this:

MaterialApp(
        // ...
        routes: {
            '/': (context) => Homepage(),
            '/second': (context) => SecondPage(),
            // etc.
        },
        initialRoute: '/', (1)
    ),
1 The initial route is the first page the app shows when opening the app

You can navigate to named routes like this:

Navigator.pushNamed(context, '/issue')
As the app grows larger the routing map may grow. To make maintainence of the routing simpler, you can store the routing map or their builder functions in a separate file. This is very common and you might see this in existing Flutter apps.

1.1. Passing data to a route

Frequently you need to pass data from one screen to another screen. For example, you may be looking at a list of training courses on https://learn.vogella.com and if the user clicks on one of these, you want to show the content of this training course.

alt

alt

This could be a user id for showing their details or just something a user entered on the first screen.

For this, Navigator.pushNamed(…​) accepts an optional named argument called arguments. You may pass any Dart object to it but is is advisable to create a "wrapper" object that has the fields you want to pass for type safety.

Creating a wrapper object might look like this:

class RouteArguments {
    final int userId;
    final String message;

    RouteArguments(this.userId, this.message);
}

Passing the argument to the route:

Navigator.pushNamed(context, '/second', arguments: RouteArguments(8, 'This is a message'));

You can access this data in the constructor of the widget which is created by this navigation call.

@override
Widget build(BuildContext context) {
    // ...
    final RouteArguments args = ModalRoute.of(context).settings.arguments;
    // ...
    // And later you may access its values directly
    int id = args.userId;
}
You can only receive these arguments via the BuildContext instance.

Dart automatically assigns the object to your RouteArguments type. But keep in mind that it can’t do so, if the object passed in was not of type RouteArguments. In this case it would throw an exception.

1.2. Handling unknown routes

You might want to generate the routes dynamically or you want to be safe and have an unknown route screen.

Luckily there is a onUnknownRoute handler in the MaterialApp constructor. This takes a function that returns a Route (e.g. a MaterialPageRoute or CuptertinoPageRoute). Its builder should return an instance of your page.

MaterialApp(
        // ...
        routes: ...,
        onUnknownRoute: (settings) {
            return MaterialPageRoute(builder: (context) => NotFoundPage());
        },
        initialRoute: '/',
    ),

2. Exercise: Navigating between two screens

2.1. Creating two screens and allowing navigation between them

Create a new empty Flutter project named navigate_screens

Create the DetailScreen class in the detail_screen.dart file.

import 'package:flutter/material.dart';

class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Detail screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('Go back!'),
        ),
      ),
    );
  }
}

Create the MasterScreen class in the master_screen.dart file.

import 'package:flutter/material.dart';
import 'package:navigate_screens/detail_screen.dart';

class MasterScreen extends StatelessWidget {
  const MasterScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: TextButton(
        onPressed: () {
          Navigator.push( (1)
            context,
            MaterialPageRoute(builder: (context) => const DetailScreen()),
          );
        },
        child: const Text("Press to navigate to next screen"),
      ),
    );
  }
}
1 The Navigator.push() call navigates to the detail screen

Adjust the main.dart to show the master screen. This screen allows to navigate to the detailed screen.

import 'package:flutter/material.dart';
import 'package:navigate_screens/master_screen.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MasterScreen(),
        ),
      ),
    );
  }
}

2.2. Sending data to the detailed screen

An easy way to send data to your detailed screen is to pass it via the constructor. To support sending data to the detailed screen adjust its constructor.

import 'package:flutter/material.dart';

class DetailScreen extends StatelessWidget {
  final String input; (1)
  const DetailScreen(this.input, {super.key}); (2)

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Detail screen'),
      ),
     body: Center(
        child: Column(
          children: [
            Text(input),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: const Text('Go back!'),
            ),
          ],
        ),
      ),
    );
  }
}
1 Variable to hold the input value
2 Adjusted constructor

Now you need to adjust the MasterScreen to provide this text to the detailed screen. We use a TextFormField for this.

import 'package:flutter/material.dart';
import 'package:navigate_screens/detail_screen.dart';

class MasterScreen extends StatefulWidget {
  const MasterScreen({super.key});

  @override
  State<MasterScreen> createState() => _MasterScreenState();
}

class _MasterScreenState extends State<MasterScreen> {
  late TextEditingController _textController;

  @override
  void initState() {
    super.initState();
    _textController = TextEditingController();
  }

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          TextFormField(
            onChanged: (value) {},
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              hintText: 'Enter a search term',
            ),
            controller: _textController,
          ),
          ElevatedButton(
            onPressed: () {
              String text = _textController.text;
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailScreen(text),
                ),
              );
            },
            child: Text("Start new screen"),
          ),
        ],
      ),
    );
  }
}

2.3. Sending data back from detailed screen to the main screen

You may want to send data back from the detailed screen to the master screen. This requires that the master screen waits for the result of the detailed screen, via the await keyword. Therefore, add the following method to the MasterScreen

  // A method that launches the SelectionScreen and awaits the result from
// Navigator.pop.
  Future<void> _navigateAndDisplaySelection(
      BuildContext context, String parameter) async {
    // Navigator.push returns a Future that completes after calling
    // Navigator.pop on the Selection Screen.
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => DetailScreen(parameter)),
    );

    // When a BuildContext is used from a StatefulWidget, the mounted property
    // must be checked after an asynchronous gap.
    if (!mounted) return;

    // After the Selection Screen returns a result, hide any previous snackbars
    // and show the new result.
    ScaffoldMessenger.of(context)
      ..removeCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text('$result')));
  }

Change your ElevatedButton code to call this method:

ElevatedButton(
            onPressed: () {
              String text = _textController.text;
              _navigateAndDisplaySelection(context, text);
            },
            child: Text("Start new screen"),
          ),

Now DetailScreen can return data in its Navigator.pop() call.

    Navigator.pop(context, "This is the return value");

3. Links and Literature