从抽屉导航屏幕的一部分 [英] Navigate part of screen from drawer

查看:57
本文介绍了从抽屉导航屏幕的一部分的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我有一个具有以下设置的应用程序:

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()),
            ],
          ),
        ));
  }
}

我想知道如何使用任何.push()方法仅从MainMenu中导航MainLoginScreen小部件.

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

(我找到了一种方法,可以通过将它与MaterialApp小部件一起包装在mainloginscreen内部的上下文中导航,但是如果我想改用MainMenu小部件(具有另一个上下文)怎么办?

(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)

推荐答案

人们普遍认为,屏幕"是路线中最上方的小部件.屏幕实例是您传递给 Navigator.of(context).push(MaterialPageRoute(builder:(context)=> HereGoesTheScreen())的对象.因此,如果它在Scaffold下,不是屏幕.也就是说,这里是选项:

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:

使用不同的屏幕.为避免代码重复,请创建 MenuAndContentScreen 类:

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.如果您不需要使用后退"按钮进行导航

您可以使用 IndexedStack 小部件.它可以包含多个小部件,一次只能看到一个.

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;
    });
  }
}

通常首选第一种方法,因为它是描述性的,这是Flutter中的主要思想.当您静态声明了整个小部件树时,可以减少出错的地方,并需要对其进行跟踪.一旦感觉到了,那真是一种享受.而且,如果您想避免返回导航,请使用ahmetakil在注释中建议的替换: Navigator.of(context).pushReplacement(...)

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(...)

第二种方法通常用于MainMenu需要在视图之间保留一些需要保留的状态,因此我们选择一个具有可互换内容的屏幕.

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.

当您特别询问嵌套的 Navigator 小部件时,可以使用它代替 IndexedStack :

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知道存在一个特定的导航器,并且应该与之交互.最好是像(2)中那样通过回调来抽象该知识,或者不像(1)中那样使用特定的导航器.Flutter实际上是将信息传递到树上而不是树上.
  2. 您有时会希望突出显示MainMenu中的活动项目,但是MainMenu很难知道导航器中当前有哪个小部件.这将增加另一个非停机交互.

对于这种交互,存在BLoC模式

在Flutter中,BloC代表业务逻辑组件.最简单的形式是一个简单的对象,它是在父窗口小部件中创建的,然后向下传递到MainMenu和Navigator,这些窗口小部件随后可以通过它发送事件并对其进行侦听.

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();
  }

这具有优点:

  • MainMenu不知道谁会收到事件.
  • 任何数量的听众都可以收听此类事件.

但是,导航器仍然存在一个基本缺陷.可以在不使用MainMenu知识的情况下使用后退"按钮或其内部小部件进行导航.因此,没有哪个变量知道现在正在显示哪个页面.要突出显示活动菜单项,您将查询导航器的堆栈,这消除了BLoC的好处.

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.

这篇关于从抽屉导航屏幕的一部分的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆