In a Nested Navigator Structure of Flutter, How do you get the a Specific Navigator?

I have this problem when I have Nested Navigators. So something like,

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: "/",
      routes: {
        '/': (context) => SomeOneView(),
        '/two': (context) => SomeTwoView(),
        '/three': (context) => SomeThreeView(),
      },
    );
  }
}

class SomeOneView extends StatefulWidget {
  @override
  _SomeOneViewState createState() => _SomeOneViewState();
}

class _SomeOneViewState extends State<SomeOneView> {
  @override
  Widget build(BuildContext context) {
   return Container(
      width: double.infinity,
      height: double.infinity,
      color: Colors.indigo,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          MaterialButton(
            color: Colors.white,
            child: Text('Next'),
            onPressed: () => Navigator.of(context).pushNamed('/two'),
          ),
        ],
      ),
    );
  }
}



class SomeTwoView extends StatefulWidget {

  @override
  _SomeTwoViewState createState() => _SomeTwoViewState();
}

class _SomeTwoViewState extends State<SomeTwoView> {
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // Some implementation
      },
      child: Navigator(
        initialRoute: "two/home",
        onGenerateRoute: (RouteSettings settings) {
          WidgetBuilder builder;
          switch (settings.name) {
            case "two/home":
              builder = (BuildContext context) => HomeOfTwo();
              break;
            case "two/nextpage":
              builder = (BuildContext context) => PageTwoOfTwo();
              break;
          }
          return MaterialPageRoute(builder: builder, settings: settings);
        },
      ),
    );
  }
}

class HomeOfTwo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      color: Colors.white,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          MaterialButton(
            color: Colors.white,
            child: Text('Next'),
            onPressed: () => Navigator.of(context).pushNamed('two/nextpage'),
          ),
        ],
      ),
    );
  }
}

class PageTwoOfTwo extends StatelessWidget {

   @override
   Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      color: Colors.teal,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          MaterialButton(
            child: Text('Next'),
            onPressed: () => Navigator.of(context).pushNamed('/three'),
          ),
        ],
      ),
    );
  }
}

So as you can see, I navigate from the Top Most Navigator provided by MaterialApp going down to the child Navigator's 'two/nextpage' which should then go to MaterialApp '/three'. The problem is that doing onPressed: () => Navigator.of(context).pushNamed('/three'), returns the Navigator of the current context which is the child Navigator. I need to access the MaterialApp's Navigator to navigate correctly. What is the right way to do this?

Also how to handle the case where the Navigator I want to access is somewhere in the middle of a stack of Navigators?


Solution 1:

Most of the time, you'll have only 2 Navigator.

Which means to obtain the nested one, do:

Navigator.of(context)

And to obtain the root one do:

Navigator.of(context, rootNavigator: true)

For more complex architecture, the easiest by far is to use GlobalKey (since you'll never read Navigators during build)

final GlobalKey<NavigatorState> key =GlobalKey();
final GlobalKey<NavigatorState> key2 =GlobalKey();

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: key,
      home: Navigator(
        key: key2,
      ),
    );
  }
}

Which you can then use this way:

key.currentState.pushNamed('foo')

Solution 2:

Actually you'd have to use nested Navigator when you have a sub navigation flow or inner journey. Please read the docs of nesting navigators.

However to get access to root navigator, you can recursively look for the parent Navigator from current Navigator and return current Navigator when it has no parent Navigator.

Example:

NavigatorState getRootNavigator(BuildContext context) {
  final NavigatorState state = Navigator.of(context);
  try {
    return getRootNavigator(state.context);
  } catch (e) {
    return state;
  }
}

//use it from any widget like
getRootNavigator(context);

EDIT:

Solution 1 :

To get a specific parent Navigator, I can think of extending current Navigator class to accept an id and find the Navigator by id. Something like:

class NavigatorWithId extends Navigator {
  const NavigatorWithId(
      {Key key,
      @required this.id,
      String initialRoute,
      @required RouteFactory onGenerateRoute,
      RouteFactory onUnknownRoute,
      List<NavigatorObserver> observers = const <NavigatorObserver>[]})
      : assert(onGenerateRoute != null),
        assert(id != null),
        super(
          key: key,
          initialRoute: initialRoute,
          onGenerateRoute: onGenerateRoute,
          onUnknownRoute: onUnknownRoute,
          observers: observers,
        );

  // when id is null, the `of` function returns top most navigator
  final int id;

  static NavigatorState of(BuildContext context, {int id, ValueKey<String> key}) {
    final NavigatorState state = Navigator.of(
      context,
      rootNavigator: id == null,
    );
    if (state.widget is NavigatorWithId) {
      // ignore: avoid_as
      if ((state.widget as NavigatorWithId).id == id) {
        return state;
      } else {
        return of(state.context, id: id);
      }
    }

    return state;
  }
}

Use NavigatorWithId instead of Navigator whenever required, like

return NavigatorWithId(
  id: 1,
  initialRoute: '/',
  onGenerateRoute: (_) =>
      MaterialPageRoute<dynamic>(builder: (_) => const YourPage()),
)

Then access it like:

NavigatorWithId.of(context, id: 1)

Solution 2 :

Pass ValueKey to the navigator and make a util function that would match key and return the required Navigator.

A function something like

NavigatorState getNavigator(BuildContext context, {bool rootNavigator = false, ValueKey<String> key}) {
  assert(rootNavigator != null);
  final NavigatorState state = Navigator.of(
    context,
    rootNavigator: rootNavigator,
  );
  if (rootNavigator) {
    return state;
  } else if (state.widget.key == key) {
    return state;
  }
  try {
    return getNavigator(state.context, key: key);
  } catch (e) {
    return state;
  }
}

Use

return Navigator(
  key: const ValueKey<String>('Navigator1'),
  initialRoute: '/',
  onGenerateRoute: (_) =>
      MaterialPageRoute<void>(builder: (_) => const RootPage()),
);

and access it like

getNavigator(context, key: const ValueKey<String>('Navigator1'))

The drawback of this method I can see as not all types of keys would be supported.

Note: I don't claim any of the above solutions to be best or optimal. These are few methods I came up with. If someone can come up with better approach, I'm eager to learn :)

Hope this helps!