【问题标题】:Navigate part of screen from drawer从抽屉中导航屏幕的一部分
【发布时间】:2021-06-15 19:05:18
【问题描述】:

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

  @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 小部件。

(我找到了一种从主登录屏幕内的上下文导航的方法,方法是用 MaterialApp 小部件包装它,但如果我想使用 MainMenu 小部件,它有另一个上下文)

【问题讨论】:

  • 要导航,您需要使用导航器,您可以添加一个按钮,然后执行 Navigator.of(context).push(MaterialPageRoute(builder: (_) => MainLoginScreen())
  • 也许我的问题措辞有误,我只想通过按下 MainMenu 小部件上的按钮来导航 MainLoginScreen 小部件。我不想拥有多个脚手架和 MainMenus。
  • Navigator.of(context).pushReplacement() 是否会起作用,它会将您导航到 MainLoginScreen,如果您要问的是,您将无法返回
  • MainLoginScreen 返回一个容器,通过在该容器上使用导航器,我将返回没有脚手架的东西(这意味着黑屏及其内容)。我想要一个 UI,其中 MainMenu 在左侧屏幕上是静态的,我更改右侧的内容。我只是想知道是否可以保持这个小部件(主菜单)原样(所以它保持它的状态和所有),而不必再次返回脚手架和相同的主菜单,以使其看起来相同并且必须在我的应用程序的每个页面上都这样做

标签: flutter navigator


【解决方案1】:

普遍认为“屏幕”是路线中最顶层的小部件。 'screen' 的实例是您传递给Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()) 的内容。所以如果它在 Scaffold 下,它就不是一个屏幕。也就是说,以下是选项:

1。如果您想使用带有“返回”按钮的导航

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

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

然后为每个屏幕创建一对屏幕和嵌套小部件:

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 小部件。它可以包含多个小部件,一次只能看到一个。

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

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

3。使用嵌套的 Navigator 小部件

正如您特别询问嵌套的 Navigator 小部件,您可以使用它来代替 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(...);

这应该可以解决问题,但这是一种不好的做法,因为:

  1. MainMenu 知道存在特定的 Navigator,它应该与之交互。最好使用(2)中的回调抽象此知识,或者不使用(1)中的特定导航器。 Flutter 的真正意义在于将信息向下传递而不是向上传递。
  2. 有时您想突出显示 MainMenu 中的活动项,但 MainMenu 很难知道导航器中当前是哪个小部件。这将增加另一个非向下交互。

对于这样的交互,有 BLoC 模式

在 Flutter 中,BLoC 代表业务逻辑组件。在其最简单的形式中,它是一个在父窗口小部件中创建的普通对象,然后传递给 MainMenu 和 Navigator,然后这些窗口小部件可以通过它发送事件并监听它。

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 不知道谁会收到该事件,如果有人的话。
  • 任何数量的侦听器都可以侦听此类事件。

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

出于所有这些原因,我仍然建议前两种解决方案之一。

【讨论】:

  • 感谢您的评论,我正在寻找的感觉有点像第二个答案,但是使用导航器而不是 IndexedStack。例如,我有MaterialApp(onGenerateRoute:function,home:Scaffold(appbar:AppBar(),body:Container(child:Row(children[MainMenu(),Navigator(onGenerateRoute:otherFunction,child:Expanded(child:MainLoginScreen())),], ), 如果我在 MainLoginScreen 中调用导航器,我可以在不使用 IndexedStack 的情况下从屏幕的一个部分导航到另一个部分,但我找不到从主菜单导航该部分的方法,即在导航器上方,
  • 您的意思是您想与来自MainMenu 的嵌套Navigator 进行交互,该MainMenu 位于小部件树的另一个分支中,并且需要沿着树向上走,然后再沿着另一个分支向下走。这是解决方案:stackoverflow.com/a/46545141/11382675 但是这很糟糕的原因有很多。我建议您使用 BLoC 模式进行此交互。它允许您发出一个事件而不知道它去哪里,然后接收另一个不关心它来自哪里的小部件。如果没有帮助,请发布一个新问题,我会在那里回答,因为这与原来的问题不符。
  • 等等,我看看它是如何适合答案的。我编辑了它。
  • 感谢您的详细解答! 3 号是我正在寻找的,而 _navigatorKey 变量是我所缺少的。
猜你喜欢
  • 1970-01-01
  • 2020-09-04
  • 1970-01-01
  • 2020-09-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-06
相关资源
最近更新 更多