美文网首页Flutter圈子Flutter中文社区Flutter学习
[译]Flutter学习笔记:BottomNavigationB

[译]Flutter学习笔记:BottomNavigationB

作者: JarvanMo | 来源:发表于2018-08-02 18:00 被阅读126次

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

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

    正文

    今天我们将看看Flutter的Navigation。

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

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

    1_yptwp6Ahe_-yhrLTg-NqwQ.png

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


    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```及其内容会滑动。 不酷。🤨

    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:


    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。 它看起来像这样的:

    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保持不变。 棒极了!😀
    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上运行应用程序,当我们按下后退按钮时,我们会发现一个有趣的现象:


    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。

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


    1_qQW2iGXiWL2F1tu6cLQfwg.gif

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

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

    Credits

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

    回顾

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

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

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

    相关文章

      网友评论

      本文标题:[译]Flutter学习笔记:BottomNavigationB

      本文链接:https://www.haomeiwen.com/subject/rhdsvftx.html