Search code examples
flutterdartnavigation-drawer

Flutter Navigator (2.0) How can I return control to the Base Router from a Nested Router on a certain Pop event?


enter image description here

I would like to use Flutter’s (2.0) navigation for routing on my mobile app. I cannot find cookbook examples and followed the recommended guide, implementing the nested router example.

NOTE ===

If a rendered view has a unique route (uri) within the app I call it a Page.

If it does not have a unique route, I call it a Screen.

========

The base router selects between pages in the app. The nested router in the “Resource Dashboard” uses the resourceViewState object to select the screen to render within the Resource Dashboard” Page. Just by using the selectedIndex as below, I can change the screen depending on which index the user has selected in a Material Design Drawer.

As a result, when the user is on any non-default screen (i.e. Details A, Details B) in the above diagram, and there is a pop event, the user is returned to the default screen. If the user pops from the default screen, they are returned to the “Select Resource” Page outside of the Nested Router. But I have one more sticky case to handle (or maybe I am not handling these cases well :))

@override
Widget build(BuildContext context) {
  int selectedIndex = resourceViewState.selectedIndex;
  return Navigator(
    key: navigatorKey,
    pages: [
      MaterialPage(
        key: ValueKey(DEFAULT_PAGE),
        child: Scaffold(
          appBar: AppBar(
            centerTitle: true,
            title: Text(‘DEFAULT’),
          ),
          drawer: ResourceDrawer(
            resourceViewState: resourceViewState,
            navKey: navigatorKey,
          ),
          body: DefaultPage(),
        ),
      ),
      if (selectedIndex != DEFAULT_PAGE) ...[
        MaterialPage(
          key: ValueKey(selectedIndex),
          child: Scaffold(
            appBar: AppBar(
              centerTitle: true,
              title: Text(_getTitle(selectedIndex)),
            ),
            drawer: ResourceDrawer(
              deviceState: deviceState,
              navKey: navigatorKey,
            ),
            body: _getScreen(selectedIndex),
          ),
        ),
      ],
    ],
    onPopPage: (route, result) {
      if (result == "return") {
        print("return ");
        return route.didPop(true);
      }
      resourceViewState.selectedIndex = DEFAULT_PAGE;
      notifyListeners();
      return route.didPop(false);
    },
  );
}

I want to navigate the user from an Alert Dialog to the “Select Resource” page when a button is clicked in the dialog. To do so I must (see 1, 2, 3 in diagram).

3 seems to be handled by the base router if the user pops from the default screen, but I also need to do so from an Alert Dialog.

  1. Pop the Alert Dialog
  2. Pop the Material Design Drawer
  3. Pop from the Nested Router to the Base Router.

This has the effect shown by the red arrow in the diagram.

I can simply use Navigator.of(context).pop() for the first 2. I am pretty sure that the Navigator used here is different from that used by the Nested Router (would love some details here). I believe this is the case, because onPopPage is not called for the NestedRouter on these events.

For 3) I have tried this strategy:

a) Call navKey.currentState!.pop(“disconnected”) from the Navigation Drawer. I pass in the navKey of the Nested Router as shown in the code above.

b) Now the onPopPage listener registered with the nested router receives the result from this pop event.

onPopPage: (route, result) {
  if (result == "return") {
    print("return ");
    return route.didPop(true);
  }
  resourceViewState.selectedIndex = DEFAULT_PAGE;
  notifyListeners();
  return route.didPop(false);
},

When I see result == "return" then I should navigate from the “Resource Dashboard” Page to the “Select Resource” Page. But I am not sure how to do it nor if it is even a good strategy to use different views on the same route (tangent).


Solution

  • This is a working solution. I pass a reference to the parent navigator key, then call pop from it. I would prefer for it to be entirely declarative but I am not sure how to implement with the nested navigator pattern.

    class ResourcePage extends StatefulWidget {
      ResourcePage({
        required this.resourceViewState,
        required this.navigatorKey,
      });
    
      final ResourceViewState resourceViewState;
      final GlobalKey<NavigatorState> navigatorKey;
    
      @override
      _ResourcePageState createState() => _ResourcePageState();
    }
    
    
    class _ResourcePageState extends State<DevicePage> {
      late DeviceDelegate _routerDelegate;
      late ChildBackButtonDispatcher _backButtonDispatcher;
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        // Defer back button dispatching to the child router
        _routerDelegate = InnerRouterDelegate(parentNavigatorKey: widget.navigatorKey, resourceViewState: widget.resourceViewState);
        _backButtonDispatcher = Router.of(context).backButtonDispatcher!.createChildBackButtonDispatcher();
        _backButtonDispatcher.takePriority();
      }
    
      @override
      void didUpdateWidget(covariant DevicePage oldWidget) {
        super.didUpdateWidget(oldWidget);
        _routerDelegate.state = widget.resourceViewState;
      }
    
      @override
      Widget build(BuildContext context) {
        return Router(
          routerDelegate: _routerDelegate,
          backButtonDispatcher: _backButtonDispatcher,
        );
      }
    }
    
    
    // InnerRouterDelegate Snippets
    
      // Constructor
    InnerRouterDelegate({
      required this.resourceViewState,
      required this.parentNavigatorKey,
    }) {
      resourceViewState.addListener(notifyListeners); // See Nested Navigation Example
    }
    
    
    // On Pop Page
    
    onPopPage: (route, result) {
      if (result == "return") {
          parentNavigatorKey.currentState!.pop();
          return route.didPop(true);
        }
        resourceViewState.selectedIndex = DEFAULT_PAGE;
        notifyListeners();
        return route.didPop(false);
    },