Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

let's say I have an app with the following setup:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: Container(
          color: Colors.grey[200],
          child: Row(
            children: [
              MainMenu(),
              Expanded(child: MainLoginScreen()),
            ],
          ),
        ));
  }
}

I would like to know how can I navigate only the MainLoginScreen widget from the MainMenu with any .push() method.

(I found a way to navigate from a context inside the mainloginscreen,by wrapping it with a MaterialApp widget, but what if I want to use the MainMenu widget instead, which has another context)

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
150 views
Welcome To Ask or Share your Answers For Others

1 Answer

There is a general agreement that a 'screen' is a topmost widget in the route. An instance of 'screen' is what you pass to Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()). So if it is under Scaffold, it is not a screen. That said, here are the options:

1. If you want to use navigation with 'back' button

Use different screens. To avoid code duplication, create MenuAndContentScreen class:

class MenuAndContentScreen extends StatelessWidget {
  final Widget child;

  MenuAndContentScreen({
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.grey[200],
        child: Row(
          children: [
            MainMenu(),
            Expanded(child: child),
          ],
        ),
      ),
    );
  }
}

Then for each screen create a pair of a screen and a nested widget:

class MainLoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MenuAndContentScreen(
      child: MainLoginWidget(),
    );
  }
}

class MainLoginWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Here goes the screen content.
  }
}

2. If you do not need navigation with 'back' button

You may use IndexedStack widget. It can contain multiple widgets with only one visible at a time.

class MenuAndContentScreen extends StatefulWidget {
  @override
  _MenuAndContentScreenState createState() => _MenuAndContentScreenState(
    initialContentIndex: 0,
  );
}

class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
  int _index;

  _MainMenuAndContentScreenState({
    required int initialContentIndex,
  }) : _contentIndex = initialContentIndex;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.grey[200],
        child: Row(
          children: [
            MainMenu(
              // A callback that will be triggered somewhere down the menu
              // when an item is tapped.
              setContentIndex: _setContentIndex,
            ),
            Expanded(
              child: IndexedStack(
                index: _contentIndex,
                children: [
                  MainLoginWidget(),
                  SomeOtherContentWidget(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _setContentIndex(int index) {
    setState(() {
      _contentIndex = index;
    });
  }
}

The first way is generally preferred as it is declrative which is a major idea in Flutter. When you have the entire widget tree statically declared, less things can go wrong and need to be tracked. Once you feel it, it really is a pleasure. And if you want to avoid back navigation, use replacement as ahmetakil has suggested in a comment: Navigator.of(context).pushReplacement(...)

The second way is mostly used when MainMenu needs to hold some state that needs to be preserved between views so we choose to have one screen with interchangeable content.

3. Using a nested Navigator widget

As you specifically asked about a nested Navigator widget, you may use it instead of IndexedStack:

class MenuAndContentScreen extends StatefulWidget {
  @override
  _MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}

class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
  final _navigatorKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.grey[200],
        child: Row(
          children: [
            MainMenu(
              navigatorKey: _navigatorKey,
            ),
            Expanded(
              child: Navigator(
                key: _navigatorKey,
                onGenerateRoute: ...
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// Then somewhere in MainMenu:
  final anotherContext = navigatorKey.currentContext;
  Navigator.of(anotherContext).push(...);

This should do the trick, however it is a bad practice because:

  1. MainMenu knows that a particular Navigator exists and it should interact with it. It is better to either abstract this knowledge with a callback as in (2) or do not use a specific navigator as in (1). Flutter is really about passing information down the tree and not up.
  2. At some point you would like to highlight the active item in MainMenu, but it is hard for MainMenu to know which widget is currently in the Navigator. This would add yet another non-down interaction.

For such interaction there is BLoC pattern

In Flutter, BLoC stands for Business Logic Component. In its simpliest form it is a plain object that is created in the parent widget and then passed down to MainMenu and Navigator, these widgets may then send events through it and listen on it.

class CurrentPageBloc {
  // int is an example. You may use String, enum or whatever
  // to identify pages.
  final _outCurrentPageController = BehaviorSubject<int>();
  Stream<int> _outCurrentPage => _outCurrentPageController.stream;

  void setCurrentPage(int page) {
    _outCurrentPageController.sink.add(page);
  }

  void dispose() {
    _outCurrentPageController.close();
  }
}

class MenuAndContentScreen extends StatefulWidget {
  @override
  _MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}

class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
  final _currentPageBloc = CurrentPageBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.grey[200],
        child: Row(
          children: [
            MainMenu(
              currentPageBloc: _currentPageBloc,
            ),
            Expanded(
              child: ContentWidget(
                currentPageBloc: _currentPageBloc,
                onGenerateRoute: ...
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _currentPageBloc.dispose();
  }
}

// Then in MainMenu:
  currentPageBloc.setCurrentPage(1);

// Then in ContentWidget's state:
  final _navigatorKey = GlobalKey();
  late final StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: _navigatorKey,
      // Everything else.
    );
  }

  void _setCurrentPage(int currentPage) {
    // Can't use this.context, because the Navigator's context is down the tree.
    final anotherContext = navigatorKey?.currentContext;
    if (anotherContext != null) { // null if the event is emitted before the first build.
      Navigator.of(anotherContext).push(...); // Use currentPage
    }
  }

  @override
  void dispose() {
    _subscription.cancel();
  }

This has advantages:

  • MainMenu does not know who will receive the event, if anybody.
  • Any number of listeners may listen on such events.

However, there is still a fundamental flaw with Navigator. It can be navigated without MainMenu knowledge using 'back' button or by its internal widgets. So there is no single variable that knows which page is showing now. To highlight the active menu item, you would query the Navigator's stack which eliminates the benefits of BLoC.

For all these reasons I still suggest one of the first two solutions.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...