Flutter/Dart: Changing pages


Here in this post we discuss how to create an App which allows to navigate back and fort between different pages.

We begin with a simple hello world app:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  home: SampleApp()
));

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Sample Application'),
        ),
        body: const Center(
          child: Text('Hello World'),
        ),
    );
  }
}

This code results in the following screen ( You can execute it in https://dartpad.dev/ or in your favourite SDK) we see the following screen:

We have started to talk about pages, but such pages (or views) are called routes in the flutter world. So we change the name of our class to RouteA and contine to talk about routes instead of pages/views:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  home: RouteA()
));

class RouteA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Route A'),
        ),
        body: const Center(
          child: Text('Welcome to Route A'),
        ),
    );
  }
}

Of course the resulting screen would look the same (but with the new text) because we have only changed the name of the class and the greeting text. So no need to show any screenshot here.

Next we add a second route:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  home: SampleApp()
));

class RouteA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Route A'),
        ),
        body: const Center(
          child: Text('Welcome to Route A'),
        ),
    );
  }
}

class RouteB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Route B'),
        ),
        body: const Center(
          child: Text('Welcome to Route B'),
        ),
    );
  }
}

The screen would still look the same, because we have no way to reach RouteB. We just see RouteA and have no way to access RouteB.

Next we replace each of the Text widgets on each of the routes with a button “Go to B” resp. a button “Go to A”. But for now just buttons without any functionality:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  home: RouteA()
));

class RouteA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Route A'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to B'),
          onPressed: () {
            // Add code to navigate to RouteB.
          },
        ),
      ),
    );
  }
}

class RouteB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Route B'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to A'),
          onPressed: () {
            // Add code to navigate to RouteA.
          },
        ),
      ),
    );
  }
}

Now we see “Route A” and the button [Go to B]. Of course clicking on the button has no effect because we haven’t yet implemented the functionality behind our buttons:

Next we add the functionality behind our first button, the button [Go to A]. For that we use the Navigator widget which has a stack memory remembering the navigation history.

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  home: RouteA()
));

class RouteA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Route A'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to B'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => RouteB()),
            );
          },
        ),
      ),
    );
  }
}

class RouteB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Route B'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to A'),
          onPressed: () {
            // Add code to navigate to RouteA.
          },
        ),
      ),
    );
  }
}

Now if we start the app we see the same screen as before, but if we click on the button [Got to B] we change to [Route B], but from there we cannot move back to [Route A] because we haven’t implemented the functionality of the button [Go to A] which we do using the same Navigator widget as for [Route A] but this time we use the method .pop instead of .push because we go backward in the navigation history instead of forward:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  home: RouteA()
));

class RouteA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Route A'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to B'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => RouteB()),
            );
          },
        ),
      ),
    );
  }
}

class RouteB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Route B'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to A'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

Now we can navigate forward and backward between the two Routes A & B