[译]Flutter学习笔记:BottomNavigationBar实现多个Navigation

简介: 这个文章解决了什么问题?最近我研究了一下Flutter,但是在使用Navigator的时候遇到了一个很头痛的问题,就是当我们去来回切换导航按钮时,Flutter会重新build,从而导致控件重新Build,从而会失去浏览历史。

这个文章解决了什么问题?

最近我研究了一下Flutter,但是在使用Navigator的时候遇到了一个很头痛的问题,就是当我们去来回切换导航按钮时,Flutter会重新build,从而导致控件重新Build,从而会失去浏览历史。这个体验肯定是不好的,后来看到了这个文章,终于解决了这个问题。
原文点这里

正文

今天我们将看看Flutter的Navigation。

但不仅仅是任何无聊的Navigation。

不,女士们,先生们,来让我们把Navigation变得有趣。
这是一个有BottomNavigationBar的app:

img_de93f8535b05a3912c28a7491f7eca91.png
1_yptwp6Ahe_-yhrLTg-NqwQ.png

我们想要的是每个选项卡都有自己的Navigation堆栈。 这样我们在切换标签时不会丢失Navigation历史记录。 如下图:


img_732ef558b8abcfd17bad20d87f5d6244.gif
multiple-navigators-BottomNavigationBar-animation.gif

如何实现此功能?长话短说:

  • 创建一个带ScaffoldBottomNavigationBar的app。
  • 在每一个Scaffold中,为每个选项卡创建一个包含一个子项的Stack
  • 每个子布局都是一个带有子NavigatorOffstage控件。
  • 不要忘记使用WillPopScope处理Android后退导航。

想要更长更有趣的解释吗? 首先,看一下免责声明:

好了,让我们开始。

一切都关于Navigator

所有Flutter应用程序都被定义为MaterialApp。 通常来说,MaterialApp位于控件树的根结点:

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.red,
      ),
      home: App(),
    );
  }
}

然后我们就可以以如下的方式定义我们的App 类:

enum TabItem { red, green, blue }

class App extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => AppState();
}

class AppState extends State<App> {

  TabItem currentTab = TabItem.red;

  void _selectTab(TabItem tabItem) {
    setState(() {
      currentTab = tabItem;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _buildBody(),
      bottomNavigationBar: BottomNavigation(
        currentTab: currentTab,
        onSelectTab: _selectTab,
      ),
    );
  }
  
  Widget _buildBody() {
    // return a widget representing a page
  }
}

这里,BottomNavigation是一个自定义控件,使用BottomNavigationBar绘制具有正确颜色的三个选项卡。 它将currentTab作为输入并调用_selectTab方法以根据需要更新状态。

有趣的部分是_buildBody()方法。 为简单起见,我们可以首先添加一个带回调的FlatButton来推送新页面:

Widget _buildBody() {
  return Container(
    color: TabHelper.color(TabItem.red),
    alignment: Alignment.center,
    child: FlatButton(
      child: Text(
        'PUSH',
        style: TextStyle(fontSize: 32.0, color: Colors.white),
      ),
      onPressed: _push,
    )
  );
}

void _push() {
  Navigator.of(context).push(MaterialPageRoute(
    // we'll look at ColorDetailPage later
    builder: (context) => ColorDetailPage(
      color: TabHelper.color(TabItem.red),
      title: TabHelper.description(TabItem.red),
    ),
  ));
}

_push()方法是如何工作的?

  • MaterialPageRoute负责创建要推送的新路由。
  • Navigator.of(context)在窗口控件树中找到Navigator,并使用它来推送新route。

你可能好奇 Navigator是从哪来的。

我们自己没有创建一个,我们的App类的父级是位于控件树根部的MaterialApp

事实证明,MaterialApp在内部创建了自己的Navigator

但是,如果我们只使用Navigator.of(context)来推送新路由,就会发生意想不到的情况。

当新页面出现时,整个``BottomNavigationBar```及其内容会滑动。 不酷。

img_b2cd80e63fc49e3c68982884ae2a9966.gif
1_k5yMOPCem_z5JZVpa6RJCQ.gif

我们真正想要的是将详细页面推到主页面上,但要将BottomNavigationBar保持在底部。

这不起作用,因为Navigator.of(context)找到BottomNavigatorBar本身的祖先。 事实上,控件树看起来像这样:

▼ MyApp
 ▼ MaterialApp
  ▼ <some other widgets>
   ▼ Navigator
    ▼ <some other widgets>
     ▼ App
      ▼ Scaffold
       ▼ body: <some other widgets>
       ▼ BottomNavigationBar

如果我们打开Flutter inspector:


img_11b731f567e8a8949b6538bfe27d08a4.png
1_zSeQkAGwARf2KtSkZqgRSg.png

如果我们可以使用不是我们BottomNavigationBar的祖先的Navigator,那么它就会按预期工作。

好的 ,Navigator,看看我们能做什么

解决方案是使用新的Navigator````包裹我们的Scaffold```对象的主体。

但在我们这样做之前,让我们介绍一下我们将用来展示最终UI的新类。

第一个类叫做TabNavigator

class TabNavigatorRoutes {
  static const String root = '/';
  static const String detail = '/detail';
}

class TabNavigator extends StatelessWidget {
  TabNavigator({this.navigatorKey, this.tabItem});
  final GlobalKey<NavigatorState> navigatorKey;
  final TabItem tabItem;

  void _push(BuildContext context, {int materialIndex: 500}) {
    var routeBuilders = _routeBuilders(context, materialIndex: materialIndex);

    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) =>
                routeBuilders[TabNavigatorRoutes.detail](context)));
  }

  Map<String, WidgetBuilder> _routeBuilders(BuildContext context,
      {int materialIndex: 500}) {
    return {
      TabNavigatorRoutes.root: (context) => ColorsListPage(
            color: TabHelper.color(tabItem),
            title: TabHelper.description(tabItem),
            onPush: (materialIndex) =>
                _push(context, materialIndex: materialIndex),
          ),
      TabNavigatorRoutes.detail: (context) => ColorDetailPage(
            color: TabHelper.color(tabItem),
            title: TabHelper.description(tabItem),
            materialIndex: materialIndex,
          ),
    };
  }

  @override
  Widget build(BuildContext context) {
    var routeBuilders = _routeBuilders(context);

    return Navigator(
        key: navigatorKey,
        initialRoute: TabNavigatorRoutes.root,
        onGenerateRoute: (routeSettings) {
          return MaterialPageRoute(
              builder: (context) => routeBuilders[routeSettings.name](context));
        });
  }
}

这个怎么起作用的?

  • 在第1-4行,我们定义了两个路由名称:// detail
    在第7行,我们定义了TabNavigator的构造函数。 这需要一个navigatorKey和一个tabItem
  • 请注意,navigatorKey的类型为GlobalKey <NavigatorState>。 我们需要这个来唯一地标识整个应用程序中的navigator(在此处阅读有关GlobalKey的更多信息)。
  • 在第22行,我们定义了一个_routeBuilders方法,它将``WidgetBuilder与我们定义的两条路径中的每一条相关联。 我们将在一秒钟内查看ColorsListPageColorDetailPage```。
  • 在第38行,我们实现了build(方法,该方法返回一个新的Navigator对象。
  • 这需要一个key和一个initialRoute参数。
  • 它还有一个onGenerateRoute方法,每次需要生成路由时都会调用该方法。 这使用了我们上面定义的_routeBuilders()方法。
  • 在第11-19行,我们定义了一个_push()方法,该方法用于使用ColorDetailPage推送细节路径。

这是ColorsListPage类:


class ColorsListPage extends StatelessWidget {
  ColorsListPage({this.color, this.title, this.onPush});
  final MaterialColor color;
  final String title;
  final ValueChanged<int> onPush;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(
            title,
          ),
          backgroundColor: color,
        ),
        body: Container(
          color: Colors.white,
          child: _buildList(),
        ));
  }

  final List<int> materialIndices = [900, 800, 700, 600, 500, 400, 300, 200, 100, 50];

  Widget _buildList() {
    return ListView.builder(
        itemCount: materialIndices.length,
        itemBuilder: (BuildContext content, int index) {
          int materialIndex = materialIndices[index];
          return Container(
            color: color[materialIndex],
            child: ListTile(
              title: Text('$materialIndex', style: TextStyle(fontSize: 24.0)),
              trailing: Icon(Icons.chevron_right),
              onTap: () => onPush(materialIndex),
            ),
          );
        });
  }
}

这个类的目的是显示可以用来输入的MaterialColor``的所有颜色阴影的ListViewMaterialColor只不过是一个有十种不同色调的ColorSwatch```。

为了完整性,这里是ColorDetailPage


class ColorDetailPage extends StatelessWidget {
  ColorDetailPage({this.color, this.title, this.materialIndex: 500});
  final MaterialColor color;
  final String title;
  final int materialIndex;

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        backgroundColor: color,
        title: Text(
          '$title[$materialIndex]',
        ),
      ),
      body: Container(
        color: color[materialIndex],
      ),
    );
  }
}

这个很简单:它只显示一个带有AppBar的页面并显示之前选择的MaterialColor。 它看起来像这样的:

img_1cb84abc5d3d6b6d6220f8b4ede27fb1.png
1_u3V51SHLSoR4q0_OD45bQg.png

将这些组装起来

现在我们有了我们自己的TabNavigator,让我们回到我们的App并使用它:

final navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TabNavigator(
        navigatorKey: navigatorKey,
        tabItem: currentTab,
      ),
      bottomNavigationBar: BottomNavigation(
        currentTab: currentTab,
        onSelectTab: _selectTab,
      ),
    );
  }
  • 首先,我们定义一个navigatorKey
  • 然后在我们的build()方法中,我们用它创建一个TabNavigator,并传入currentTab
    如果我们现在运行应用程序,我们可以看到推送在选择列表项时正常工作,并且BottomNavigationBar保持不变。 棒极了!
img_732ef558b8abcfd17bad20d87f5d6244.gif
multiple-navigators-BottomNavigationBar-animation.gif

但是有一个问题。 在标签之间切换似乎不起作用,因为我们总是在Scaffold主体内显示红色页面。

多个Navigator

这是因为我们已经定义了一个新的导航器,但这是在所有三个选项卡中共享的。

记住:我们想要的是每个标签的独立导航堆栈!

我们解决这个问题:

class App extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => AppState();
}

class AppState extends State<App> {

  TabItem currentTab = TabItem.red;
  Map<TabItem, GlobalKey<NavigatorState>> navigatorKeys = {
    TabItem.red: GlobalKey<NavigatorState>(),
    TabItem.green: GlobalKey<NavigatorState>(),
    TabItem.blue: GlobalKey<NavigatorState>(),
  };

  void _selectTab(TabItem tabItem) {
    setState(() {
      currentTab = tabItem;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(children: <Widget>[
        _buildOffstageNavigator(TabItem.red),
        _buildOffstageNavigator(TabItem.green),
        _buildOffstageNavigator(TabItem.blue),
      ]),
      bottomNavigationBar: BottomNavigation(
        currentTab: currentTab,
        onSelectTab: _selectTab,
      ),
    );
  }

  Widget _buildOffstageNavigator(TabItem tabItem) {
    return Offstage(
      offstage: currentTab != tabItem,
      child: TabNavigator(
        navigatorKey: navigatorKeys[tabItem],
        tabItem: tabItem,
      ),
    );
  }
}

几点说明:

  • 在第9-13行,我们定义了一个全局导航键的地图。 这是我们确保使用多个导航器所需的。
  • 我们的脚手架的身体现在是一个有三个孩子的堆栈。
  • 每个子项都在_buildOffstageNavigator()方法中构建。
  • 这将Offstage控件与子TabNavigator一起使用。 如果正在呈现的选项卡与当前选项卡不匹配,则offstage属性为true。
  • 我们将navigatorKey [tabItem]传递给TabNavigator,以确保每个选项卡都有一个单独的导航键。
  • 如果我们编译并运行应用程序,现在一切都按照预期的方式工作。 我们可以独立地推送/弹出每个导航器,并且后台导航员保持他们的状态。

One more thing

如果我们在Android上运行应用程序,当我们按下后退按钮时,我们会发现一个有趣的现象:


img_edb46ad2fde31f06e2e2a796fcf16ff2.gif
1_4_rjL1Hh_zKHJHjO4MNOIg.gif

app消失了,我们回到了主屏幕!

这是因为我们没有指定应该如何处理后退按钮。

我们来解决这个问题:

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async =>
          !await navigatorKeys[currentTab].currentState.maybePop(),
      child: Scaffold(
        body: Stack(children: <Widget>[
          _buildOffstageNavigator(TabItem.red),
          _buildOffstageNavigator(TabItem.green),
          _buildOffstageNavigator(TabItem.blue),
        ]),
        bottomNavigationBar: BottomNavigation(
          currentTab: currentTab,
          onSelectTab: _selectTab,
        ),
      ),
    );
  }

这是通过WillPopScope完成的,该控件控制如何解除路由。 看一下WillPopScope的文档:

注册用户否决尝试的回调以解除封闭的/// [ModalRoute]
在第4行,我们定义一个onWillPop()回调,如果当前导航器可以弹出则返回false,否则返回true。

如果我们再次运行应用程序,我们可以看到按下后退按钮会解除所有推送路线,只有当我们再次按下它时我们才会离开应用程序。


img_a8c0a5aa697a59a804ea54a43143b176.gif
1_qQW2iGXiWL2F1tu6cLQfwg.gif

需要注意的一点是,当我们在Android上推送新路线时,会从底部滑入。 相反,惯例是在iOS上从右侧滑入。

此外,由于某些原因,Android上的过渡有点紧张。 我不确定这是否是一个模拟器问题,它在真实设备上看起来不错。

Credits

积分转到]Brian Egan](https://github.com/brianegan)找到一种让Navigator工作的方法。 他的想法是使用Stack with Offstage来保持导航器的状态。

回顾

今天我们学习了很多关于Flutter导航的知识,以及如何结合BottomNavigationBarStackOffstageNavigator控件来实现多个导航堆栈。

使用Offstage小部件可确保我们的所有导航器保留其状态,因为它们保留在控件树中。 这可能会带来一些性能损失,因此如果您选择使用它,我建议您分析您的应用。

可以在此处找到本文的完整源代码

目录
相关文章
|
JSON Dart IDE
Flutter实现国际化
开发一个App,如果我们的App需要面向不同的语种(比如中文、英文、繁体等),那么我们需要对齐进行国际化开发
1398 0
Flutter实现国际化
|
6月前
|
设计模式 缓存 Dart
Flutter学习笔记&学习资料推荐,15分钟的字节跳动视频面试
Flutter学习笔记&学习资料推荐,15分钟的字节跳动视频面试
|
4月前
flutter-provider学习笔记
flutter-provider学习笔记
Flutter 底部导航栏BottomNavigationBar,并关联PageView实现滑动切换
Flutter 底部导航栏BottomNavigationBar,并关联PageView实现滑动切换
352 0
flutter系列之:创建一个内嵌的navigation
我们在flutter中可以使用Navigator.push或者Navigator.pushNamed方法来向Navigator中添加不同的页面,从而达到页面调整的目的。 一般情况下这样已经足够了,但是有时候我们有多个Navigator的情况下,上面的使用方式就不够用了。比如我们有一个主页面app的Navigator,然后里面有一个匹配好友的功能,这个功能有多个页面,因为匹配好友功能的多个页面实际上是一个完整的流程,所以这些页面需要被放在一个子Navigator中,并和主Navigator区分开。
|
Dart IDE 开发工具
Flutter 图文并茂列表实现
Flutter使用 ListView 完成列表的构建,界面实现的关键工作实际是布局子元素的拆分。剩下的实现方式存在多种,看各人喜好。但是,需要注意避免过多嵌套导致代码不好维护,并需要提高复用性。
878 2
Flutter 图文并茂列表实现
|
数据安全/隐私保护 UED
Flutter 使用自定义fluro 路由实现访问权限控制
本篇介绍了利用 Fluro 路由管理实现路由权限拦截的两种方式,两种方式各有好处,使用过程中可以根据实际情况决定使用哪一种方法。
630 1
Flutter 使用自定义fluro 路由实现访问权限控制
|
存储 数据安全/隐私保护
Flutter App页面路由及路由拦截实现
直接使用页面跳转会带来诸多缺陷,通过路由管理可以降低页面耦合,提高代码的可维护性和权限控制。本篇介绍了 Flutter 的路由管理和拦截实现。
1361 1
Flutter App页面路由及路由拦截实现
|
移动开发 Android开发 索引
flutter之从零开始搭建(一)之 BottomNavigationBar
flutter之从零开始搭建(一)之 BottomNavigationBar
181 0
flutter之从零开始搭建(一)之 BottomNavigationBar
|
存储 前端开发
Flutter 实现多选底部弹窗
本篇介绍了底部弹窗实现多选的方式,其中实现的方式还可以有很多种,例如直接在自定义组件中使用有状态组件。这里介绍的方法可以作为一个参考,通过动态构建有状态组件能够简单快速地实现底部弹窗的多选功能。
692 0
Flutter 实现多选底部弹窗