28

I have been using Navigator.pop and passing data back 1 screen with ease. However, if I use Navigator.popUntil, what is an acceptable way to pass an object back to the destination screen?

Zoe
  • 27,060
  • 21
  • 118
  • 148
Josh Kahane
  • 16,765
  • 45
  • 140
  • 253

5 Answers5

24

You can using arguments in RouteSettings to pass data back to a specific Route.

For example:


// in MaterialApp
MaterialApp(
   onGenerateRoute: (settings) {
      switch (settings.name) {
         case '/':
            return MaterialPageRoute(
                     settings: RouteSettings(name: '/', arguments: Map()),  // (1)
                     builder: (_) => HomePage()
                  );
      }
   }
)

// in HomePage
Navigator.of(context).push(MaterialPageRoute(builder: (_) => StuffPage())
                     .then(_) {
                        final arguments = ModalRoute.of(context).settings.arguments as Map;
                        final result = arguments['result'];
                     };

// in StuffPage
Navigator.of(context).popUntil((route) {
   if (route.settings.name == '/') {
      (route.settings.arguments as Map)['result'] = 'something';
      return true;
   } else {
      return false;
   }
});

Note that: you have to initialize arguments or it will be null, that's what is the purpose of (1)

hunghd
  • 1,274
  • 11
  • 21
2

There is none. It also doesn't make sense because the result should be given back to the route that pushed the route.

If you really have to do this, use multiple pop calls:

// Widget of Route A:
String resultOfC = await Navigator.push(context, routeB);


// Widget of Route B:
String resultOfC = await Navigator.push<String>(context, routeC);
Navigator.pop(context, resultOfC); // pass result of C to A


// Widget of Route C:
Navigator.pop(context, 'my result'); // pass result to B
boformer
  • 28,207
  • 10
  • 81
  • 66
  • this will get displayed in view while the `popUntil` will not. So it would be better if we able to popUntil and pass the data also. – Dinesh Balasubramanian Aug 29 '18 at 16:23
  • Yeah, there are other ways. I would like to know what OP is trying to do. It's probably a conceptual issue how he is using the `Navigator`. – boformer Aug 30 '18 at 10:03
  • 1
    This appears to be the most clean approach even though it causes tight coupling. I don't think the coupling is an issue though as it's two layers deep and probably won't occur often in most apps. If you have to go three layers back, I'd question this approach as too unwieldy. – AutoM8R Sep 09 '21 at 19:01
0

plus @hunghd answer's make sure that the screen settings that you will popback must not have a const key. otherwise you will be faced following error:

Unhandled Exception: Unsupported operation: Cannot modify unmodifiable map

I just removed the const key from settings on following code where the user will be popped back.

 case HomeViewRoute:
      return MaterialPageRoute(
        builder: (context) => HomePage(),
        settings: RouteSettings(name: HomeViewRoute, arguments:  {}),
      );
aligur
  • 3,387
  • 3
  • 34
  • 51
0

Here is the solution for single shot argument reading. As the solution proposed by @hunghd does not take this into account. The trick here is to consume arguments once, not many times as it might lead to wrong behaviours.

If we want to "go back with single result" we should do the following thing. We can draft the following engine in three steps:

First step - setup in the Main.app:

MaterialApp(
   onGenerateRoute: (settings) {
       switch (settings.name) {
           case Routes.HOME: // Or your other path, like "/"
               return generateRouteWithMapArguments(
                   Routes.HOME,
                   HomePage(), // A page Widget
               );
       // Other switch cases
      }
   }
)

// util:
static MaterialPageRoute generateRouteWithMapArguments(
    String routeName,
    Widget page,
  ) =>
      MaterialPageRoute(
        settings: RouteSettings(name: routeName, arguments: Map()),
        builder: (context) => page,
      );

Second step - Performing navigation:

const String ARGUMENTS_REMOVED = "ARGUMENTS_REMOVED";


void _navigateToDetails(BuildContext context) async {
    // navigate here help with the help of Navigator.of(context).push() or GetX
    await Navigator.of(context).push(...)
    var result = ArgsUtils.consumeArgument(
      context,
      ARGUMENTS_REMOVED,
      () {
        // HANDLE SINGLE SHOT HERE
      },
    );
  }

// utils:
class ArgsUtils {
  static T readArgs<T>(BuildContext context) {
    return ModalRoute.of(context)!.settings.arguments as T;
  }

  static bool consumeArgument(
    BuildContext context,
    String argument,
    VoidCallback onArgument,
  ) {
    var map = readArgs<Map>(context);
    if (map.containsKey(argument)) {
      onArgument.call();
      map.clear();
      return true;
    }
    return false;
  }
}

*Third step - passing arguments back:

// passing back. Call this method two pages after navigating deeper from HOME 
NavigatorUtils.popTwoPagesWithArgument(
    context,
    routeName: Routes.HOME,
    argument: ARGUMENTS_REMOVED,
);


// util:
class NavigatorUtils {
  // you can pop more pages here or add different predicate:
  static void popTwoPagesWithArgument(
    final BuildContext context, {
    required final String routeName,
    required final String argument,
  }) {
    int count = 0;
    Navigator.of(context).popUntil((route) {
      if (route.settings.name == routeName &&
          route.settings.arguments != null) {
        (route.settings.arguments! as Map)[argument] = true;
      }
      return count++ >= 2;
    });
  }
}

Of course this solution passes redundant bool. And this raises questions whether a Map is needed. For single-shot cases I believe that this can be improved by using Set<String> instead of Map.

Michał Dobi Dobrzański
  • 1,449
  • 1
  • 20
  • 19
-1

For workaround solution until the Flutter team provides it, you can do it using shared_preferences.

  1. Add class with function to get and save value to shared_preferences.
  static const isBackFromPopUntil = "isBackFromPopUntil";

  Future<void> saveIsBackFromPopUntil(bool isBack) async =>
      await preferences.setBool(isBackFromPopUntil, isBack);

  bool getIsBackFromPopUntil() =>
      preferences.getBool(isBackFromPopUntil) ?? false;
  1. For example you need to check "someting" when modal bottom sheet is closed from popUntil, otherwise not.
  void _openBottomSheet(BuildContext context) async {
    await showCupertinoModalBottomSheet(
      context: context,
      isDismissible: true,
      ...
    );
    final isBack = _prefHelper.getIsBackFromPopUntil();
    if (isBack) {
      // do stuff
      await _prefHelper.saveIsBackFromPopUntil(false);
    }
  }
  1. You back from the screen but using popUntil.
void _popUntilDetailSavingPlan(BuildContext context) async {
    final routeName = 'your name route that have bottom sheet (in the number 2 above)';
    await _prefHelper.saveIsBackFromPopUntil(true);
    Navigator.popUntil(context, ModalRoute.withName(routeName));
  }
R Rifa Fauzi Komara
  • 1,915
  • 6
  • 27
  • 54