Can't pop value when PushReplacement is used in intermediate page

Viewed 1123

I have a page that pushes another page and waits for the returned/popped value to process. I'd like this second page to be able to delegate if possible (let another page return the value). Something like this:

// page 1
Navigator.of(context).push(Page2()).then((value) => print(value)); // process value; perhaps setState or whatever

// page 2
Navigator.of(context).pushReplacement(Page3()); // let another page handle the rest

// page 3
Navigator.of(context).pop(99);

However, the future on the first page never returns and the value is never printed (or processed in whichever way I see fit). The reason for the pushReplacement is I don't want the user to go back to page 2 if they've made it to the third page.

Here's a sample flutter app you can try on https://dartpad.dartlang.org/flutter to see what I mean.

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Page1(),
    );
  }
}

class Page1 extends StatefulWidget {
  @override
  Page1State createState() => Page1State();
}

class Page1State extends State<Page1> {
  List<int> numbers = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            Text('Page 1'),
            ...numbers.map((number) => Text(number.toString())),
            RaisedButton(
              child: Text('go to page 2'),
              onPressed: () {
                Navigator.of(context)
                    .push(MaterialPageRoute(builder: (context) => Page2()))
                    .then((number) {
                  if (number != null) {
                    setState(() {
                      numbers.add(number);
                    });
                  }
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            Text('Ready?'),
            RaisedButton(
              child: Text('Yes'),
              onPressed: () {
                Navigator.of(context).pushReplacement(
                    MaterialPageRoute(builder: (context) => Page3()));
              },
            ),
            RaisedButton(
              child: Text('No'),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        ),
      ),
    );
  }
}

class Page3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            Text('Pick a number'),
            ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(
              (number) => RaisedButton(
                child: Text(number.toString()),
                onPressed: () => Navigator.of(context).pop(number),
              ),
            ),
            RaisedButton(
              child: Text('Cancel'),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        ),
      ),
    );
  }
}

I must clarify the use case. The first page will fetch data from a server or whatever, so I'd use a FutureBuilder or similar. The second page is a form to create/add additional items. The form is submitted and if the server is happy with the data, the response will contain the newly created data. I then want to replace the current form page with a success page showing the data. When the user pops this page, I want the first page's list to be updated with the data.

Fancy state management like Bloc, ValueNotifier, or similar could be used but this page is short lived and I'd like to implement this feature with as little boilerplate code as possible. Also, I have found that using these state management classes require being added above/before the MaterialApp if other pages need access. For a page which might be buried deep in my app, I'd like to avoid adding a Bloc/ValueNotifier/whatever in main. That just feels like giving it too much importance.

2 Answers

You can give pushReplacement a result to return as if it was popped.

So what you can do in this case is give it again a future (for instance using a Completer) that the original pushing widget can await again for the real result

void method() async {
  final completer = Completer();
  final result = await navigator.pushReplacement(route, result: completer.future);
  completer.complete(result);
}

You can't use navigator.pushReplacement in this case, because it triggers the callback in Page1 to run and that won't be available anymore. The callback needs to get the value back from / through Page2; the 'then' in Page1 is called only if Navigator.pop(context) is called from Page2.

  1. First of all in Page2:
// replace this
Navigator.of(context).pushReplacement(Page3());
// to this
Navigator.of(context).push(MaterialPageRoute(builder: (context) => Page3()));
  1. Now you need to pop 2 times to get from Page3 to Page1:

    Unfortunately this is not possible - simply - with Navigator.of(context).popUntil.

    A) ugly and risky version - named routes are not required for this

    Navigator.of(context).pop(); 
    Navigator.of(context).pop(number);
    

    Maybe you should include the return value (number) in the first pop too, to make sure that it arrives correctly. It is not certain that the pop in the second line will end later with this solution!

    B) wait for navigation response in Page2 too (as in Page1), and pop one time in Page3, and then in Page2 - named routes are not required for this

    Maybe you could implement something like this (even with named routes): Flutter popuntil with return data

    C) set Page1 to named route and use route settings to pass data

    (i set the mainAxisAlignment parameter several places in your code because the render didn't look good)

    import 'package:flutter/material.dart';
    
    final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
          debugShowCheckedModeBanner: false,
          home: Page1(),
          initialRoute: '/page1',
          onGenerateRoute: (settings) {
            switch (settings.name) {
              case '/page1':
                return MaterialPageRoute(
                  settings: RouteSettings(name: '/page1', arguments: Map()),
                  builder: (_) => Page1(),
                );
              default:
                return null;
            }
          },
        );
      }
    }
    
    class Page1 extends StatefulWidget {
      @override
      Page1State createState() => Page1State();
    }
    
    class Page1State extends State<Page1> {
      List<int> numbers = [];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Page 1'),
                ...numbers.map((number) => Text(number.toString())),
                RaisedButton(
                  child: Text('go to page 2'),
                  onPressed: () {
                    Navigator.of(context).push(MaterialPageRoute(builder: (_) => Page2())).then((_) {
                      final Map arguments = ModalRoute.of(context).settings.arguments as Map;
                      final int number = arguments['number'] ?? null;
                      if (number != null) {
                        setState(() {
                          numbers.add(number);
                        });
                      }
                    });
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class Page2 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Ready?'),
                RaisedButton(
                  child: Text('Yes'),
                  onPressed: () {
                    Navigator.of(context).push(MaterialPageRoute(builder: (context) => Page3()));
                  },
                ),
                RaisedButton(
                  child: Text('No'),
                  onPressed: () => Navigator.of(context).pop(),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class Page3 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Pick a number'),
                ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(
                      (number) => RaisedButton(
                    child: Text(number.toString()),
                    onPressed: () {
                      Navigator.of(context).popUntil((route) {
                        if (route.settings.name == '/page1') {
                          (route.settings.arguments as Map)['number'] = number;
                          return true;
                        } else {
                          return false;
                        }
                      });
                    },
                  ),
                ),
                RaisedButton(
                  child: Text('Cancel'),
                  onPressed: () => Navigator.of(context).pop(),
                ),
              ],
            ),
          ),
        );
      }
    }
    

Other options 1

Use named routes as much as you can and implement some higher level, stricter route, state, event handling functionality with bloc pattern or provider pattern.

It would take a long time to explain these now, just look for these.

Other options 2

Anyway, to me this seems like an xy problem, a design error. This whole problem seems to stem from a design flaw. If the given example code was not created just for the test, then i think you should replace Page2 with a flutter dialog or you have many other opportunities to not get into this extreme situation.

Related