Flutter Navigator pushReplacement and popUntil

eye-catch Dart and Flutter

Page transition is definitely required for all applications. Flutter offers multiple ways to do it, so I tried to use them. I will show you the basics in this article. What we try is basically the following.

cases
Sponsored links

Understand Page Stack by the simplest page transition

First of all, let’s start with the simplest page transition. Use Navigator class to move the page.

// Go to TransitionFirstPage page
Navigator.push(
    context,
    // Instantiate the new page 
    MaterialPageRoute(builder: (context) => TransitionFirstPage()),
);
// Back
Navigator.pop(context);

Navigator class manages the page transition by stack. When the push function is called, it pushes the page to the top of the stack. When the pop function is called, it removes the item from the top.

navigator-stack

This is important to know. If we call push function multiple times to traverse different pages, multiple items are on the stack.

Sponsored links

Get the result from the another page

If we want to input some data on a different page and use them on the original page, we need to somehow get the result. In this case, set the data to the second parameter when calling pop function.

class TransitionFirstPage extends StatelessWidget {
    ...

  Widget _button1(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Back"),
        onPressed: () {
          Navigator.pop(context, "From first page");
        },
      ),
    );
  }
}

If the second parameter is specified, Navigator.push function returns the value. However, we have to call it with await keyword.

class _PageTransitionState extends State<PageTransition> {
    String? text = "Initial state";
    ...

    Widget _button1(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        final result = await Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => TransitionFirstPage()),
        );
        setState(() {
          text = result;
        });
      },
      child: Text("Go to First Page"),
    );
  }
}

The result is shown at the top of the screen. The text is “Initial state” at first but it changes to “From first page” after “Back” button is pressed.

Even if we call pop function with the result, it receives null data if the default back button at the top-left side is pressed.

Pass data to another page

The next is in the opposite way. Pass data to the next page. push function doesn’t have a parameter to pass data but we can pass it via MaterialPageRoute.settings.arguments. Since this is Object type, we can pass whatever we want.

class TransitionFirstPage extends StatelessWidget {
  ...

  Widget _button2(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Go to second page by push"),
        onPressed: () async {
          await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => TransitionSecondPage(),
              // Set the data here
              settings: RouteSettings(arguments: "Data From first page"),
            ),
          );
        },
      ),
    );
  }
}

The settings data can be retrieved via ModalRoute but it can be null. Therefore, we need to do null check here.

class _TransitionSecondPageState extends State<TransitionSecondPage> {
  String? text = "Initial state";
  @override
  Widget build(BuildContext context) {
    final settings = ModalRoute.of(context)?.settings;
    if (settings == null || settings.arguments == null) {
      text = "No data received";
    } else {
      final data = settings.arguments;
      if (data is String) {
        text = data;
      } else {
        text = "unknown data type";
      }
    }
  ...
}

arguments can also be null and it is an Object type. Therefore, data type check is also necessary. The data is shown at the top.

Not Let a user go back to the previous page

If we don’t want to let a user go back to the previous page, we need to remove the previous page from the stack. We don’t actually need to remove it. pushReplacement function does it for us.

class TransitionFirstPage extends StatelessWidget {
  ...

  Widget _button3(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Replace this page with second page"),
        onPressed: () async {
          await Navigator.pushReplacement(
            context,
            MaterialPageRoute(builder: (context) => TransitionSecondPage()),
          );
        },
      ),
    );
  }
}

Let’s check the difference between push and pushReplacement. The new page is added to the stack when calling push function but pushReplacement replaces the item which is on the top of the stack with the new page.
When pop function is called, the top item is removed from the stack and the next item is shown on the screen. It is “Page transition sample” page here.

push-and-pushreplacement

The behavior looks like this. When the back button is pressed, it goes to “Page Transition sample” page because “First page” is replaced with “Second page”.

How to set result for pushReplacement

Did you recognize that “From second page” was shown at the top when we went back to “Page Transition sample” page? If we want to pass data to the page, we need to set it.

class TransitionFirstPage extends StatelessWidget {
  ...

  Widget _button3(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Replace this page with second page"),
        onPressed: () async {
          // Prepare Completer
          final completer = Completer();
          final result = await Navigator.pushReplacement(
            context,
            MaterialPageRoute(builder: (context) => TransitionSecondPage()),
            // Set the result
            result: completer.future,
          );
          // Call complete
          completer.complete(result);
        },
      ),
    );
  }
}

On “Page Transition sample” page, it calls Navigator.push function to go to “First page”. It means that the process proceeds if the “First page” is removed from the stack. To block the process, we need to call pushReplacement with await keyword and set the result that is set on the next page.

How to go back to the home

We can call as many push functions as we want to traverse multiple pages. To go back to the home, we can call pop function as many times as push function called. However, we don’t have to do it. Use popUntil function instead.

class TransitionThirdPage extends StatelessWidget {
  ...
  Widget _button1(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Back to home"),
        onPressed: () {
          Navigator.popUntil(context, (route) => route.isFirst);
        },
      ),
    );
  }
}

popUntil function repeats to call pop function until the second function returns true.

It went back to the top page of the application.

Go back to the desired page

If we want to go back to the desired page, we need to implement it in a different way. Let’s go back to the “First page” from the “Third page”. Set ModalRoute.withName to the second parameter.

class TransitionThirdPage extends StatelessWidget {
  ...
  Widget _button2(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Back to first page"),
        onPressed: () {
          // it shows black screen when the route doesn't exist on the stack
          Navigator.popUntil(context, ModalRoute.withName("/transition1"));
        },
      ),
    );
  }
}

Black screen is shown when calling popUntil with ModalRoute.withName

I set the name “/transition1”. This name must be specified somewhere. Otherwise, a black screen is shown like this.

black-screen

It needs to be set when calling push function. It seems that the name is used in the next page. This example sets the name on the top page but the name is for the next page.

class _PageTransitionState extends State<PageTransition> {
  ...
  Widget _button1(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        final result = await Navigator.push(
          context,
          MaterialPageRoute(
            // Set the name here
            settings: RouteSettings(
              name: "/transition1",
            ),
            builder: (context) => TransitionFirstPage(),
          ),
        );
        setState(() {
          text = result;
        });
      },
      child: Text("Go to First Page"),
    );
  }
}

Once we set the name, we can go back to the “First page” from the “Third page”.

Setting routes and go to a page with the name

We have instantiated a page in push functions so far. It’s also one option but we can use a name instead. My sample application defines MaterialApp as follows.

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: "App",
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyApp(),
        initialRoute: "/home",
        // define routes here
        routes: {
          "/home": (context) => MyApp(),
          "/transition": (context) => PageTransition(),
        },
        onGenerateRoute: generateRoute,
      ),
    ),
  );
}

Currently, there are two names. “/transition” is the top page of this page transition sample. We can now go to the page wherever we are if we call pushNamed function. Let’s see the following 3 examples.

class _TransitionSecondPageState extends State<TransitionSecondPage> {
  ...
  Widget _backToTop1(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Back to Top by pushNamed"),
        onPressed: () {
          Navigator.pushNamed(context, "/transition");
        },
      ),
    );
  }

  Widget _backToTop2(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Back to Top by pushNamedAndRemoveUntil"),
        onPressed: () {
          Navigator.pushNamedAndRemoveUntil(
            context,
            "/transition",
            (route) => route.isFirst,
          );
        },
      ),
    );
  }

  Widget _backToTop3(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: Text("Back to Top by popAndPushNamed"),
        onPressed: () {
          Navigator.popAndPushNamed(context, "/transition");
        },
      ),
    );
  }

Let’s see the difference.

difference-between-pushNamed-friends

The stack is different for each. I didn’t add home to the image but actually, it is at the bottom. pushNamedAndRemoveUntil has only one item on the stack. If it is removed from the stack, the application home is shown if we press the back button there.

This video shows for pushNamed. It goes back to the Second page again.

But ModalRoute.withName doesn’t work because the name is null. This code shows a black screen.

// not work
Navigator.popUntil(context, ModalRoute.withName("/transition"));

pushNamed adds the page to the stack. Therefore, it goes back to the “Second page” again when the back button is pressed on the top page.

Define onGenerateRoute

There are some pages that are not defined in MaterialApp.routes but maybe we want to go to the page with the name. How can we implement it? If pushNamed friends function is called, MaterialApp.onGenerateRoute is called. However, remember that the function is called only if the specified name is NOT contained in the MaterialApp.routes.

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        ...
        routes: {
          "/home": (context) => MyApp(),
          "/transition": (context) => PageTransition(),
        },
        onGenerateRoute: generateRoute,
      ),
    ),
  );
}

Route<dynamic>? generateRoute(RouteSettings settings) {
  switch (settings.name) {
    case "/transition1":
      return MaterialPageRoute(
          settings: settings, builder: (context) => TransitionFirstPage());
    case "/transition2":
      return MaterialPageRoute(
          settings: settings, builder: (context) => TransitionSecondPage());
    case "/transition3":
      return MaterialPageRoute(
          settings: settings, builder: (context) => TransitionThirdPage());
  }
}

I defined the 3 cases above. Let’s use it from the top page.

class _PageTransitionState extends State<PageTransition> {
  ...
  Widget _button2(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        await Navigator.pushNamed(
          context,
          "/transition3",
          arguments: PageArguments("Transition Top Page!!"),
        );
        setState(() {
          text = "----";
        });
      },
      child: Text("Go to Third Page by pushNamed"),
    );
  }
}

It goes to the “Third page” from the top page. It shows a black screen when
“Back to first page” button is pressed. It tries to go back to the page with this code.

Navigator.popUntil(context, ModalRoute.withName("/transition1"));

However, “/transition1” is not on the stack. Therefore, popUntil function calls pop function until it removes “/transition1” page which doesn’t exist. Therefore, it shows a black screen.

End

There are many functions that we can use for page transition. This article covers only basics I guess.

If you want to try it yourself, you can clone my repository.

https://github.com/yuto-yuto/flutter_samples/blob/main/lib/page_transition.dart

Comments

Copied title and URL