Code, flutter

Flutter: Re-using named routes after they get disposed/pop'ed

So I hit a problem when I was trying to re-use named routes once I had already disposed or .pop()'ed them. Since it couldn't really find a satisfactory answer during my searches (likely because I didn't quite understand what question I wanted to ask), I decided to post my answer here, and hopefully it can help someone else. So, if you are experiencing this issue right now, don't worry the fix is quite simple  - at least how I did it.

The issue

I wanted to navigate to another route in my app and be able to go back to the previous route once done. On the surface this seemed pretty easy.

I just Navigator.of(context).pushNamed('/page2'); and once I am done with /page2 I would simply Navigator.of(context).pop(); within the /page2 Widget and that should be it.

However, if I then wanted to navigate back to /page2 at any later time, my app would crash with the error:

Cannot install a PageRouteBuilder<dynamic> after disposing it.
'package:flutter/src/widgets/routes.dart':
Failed assertion: line 176 pos 12: '!_transitionCompleter.isCompleted'

So it turned out, I also needed to change how my routes were generated for this to work.

The Code

Before I can explain exactly what I had to change, let me walk though the logic, using a few snippets.

On my /page1 widget I wanted a button which would take my to another part of my app, /page2, something similar to this:

// Page 1 widget

@override
Widget build(BuildContext context) {
  return GestureDetector(
    onTap: () {
      Navigator.of(context).pushNamed('/page2'); // <-------
    },
  );
}

From this /page2 widget, I wanted to be able to go back to the previous route, so I added a Back button in this widget:

// Page 2 widget

@override
Widget build(BuildContext context) {
  return CMaterialButton(
    onPressed: () {
      Navigator.of(context).pop(); // <-------
    },
    child: Text('Go Back'),
  );
}

Because I am using named routes, I have a routes file which generates the routes used in the app, it looks something like this:

import 'package:flutter/material.dart';

import 'views/page1.dart';
import 'views/page2.dart';

Map<String, PageRouteBuilder> routes = <String, PageRouteBuilder>{
  '/': PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return Page1();
    },
  ),
  '/page2': PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return Page2();
    },
  ),
};

The routes are handled by my MaterialApp, which looks similar to this:

import 'routes.dart';

MaterialApp(
  title: 'My App',
  initialRoute: '/',
  onGenerateRoute: (settings) {
    // return the route if found
    if (routes.containsKey(settings.name)) {
      return routes[settings.name];
    }

    // otherwise return error page
    return routes['/unknwon-route'];
  },
),

The Fix

The problem turned out to be how I generate my routes. In my current routes file, I only generate the routes once. So when I pop the route from the Navigator stack, it is gone forever. However this can be solved very easily, by generating your route every time it is needed.

So, in my routes.dart file, these are the changes I had to first change the type definition from PageRouteBuilder to Function:

// The old way:
Map<String, PageRouteBuilder> routes = <String, PageRouteBuilder>

// The new way
Map<String, Function> routes = <String, Function>

I then changed each list item to return a function (by prefixing it with () =>, which returns a new PageRouteBuilder every time the function runs:

// The old way
'/': PageRouteBuilder(
  pageBuilder: (context, animation, secondaryAnimation) {
    return Page1();
  },
)

// The new way
'/': () => PageRouteBuilder(
  pageBuilder: (context, animation, secondaryAnimation) {
    return Page1();
  },
)

Now, I did have to change my MaterialApp slightly as well, since routes no longer return a PageRouteBuilder but a function, we need to call said function every time as well:

MaterialApp(
  ...
  onGenerateRoute: (settings) {
    // return the route if found
    if (routes.containsKey(settings.name)) {
      return routes[settings.name](); // <-----------
    }

    // otherwise return error page
    return routes['/unknwon-route'](); // <-----------
  },
),

Thoughts/Cautions

At time of writing, I don't yet know if this could potentially add multiple routes with the same name to your navigator stack, or if there are any other issues which could come from this. I am still fairly inexperienced with Flutter so please be critical of this approach, as chances are I could easily have missed something.

Please reach out if you see anything wrong with this approach, I would be more than happy to know!