Flutter圈子Flutter中文社区Flutter学习

[译]Flutter学习笔记:BottomNavigationB

2018-08-02  本文已影响126人  JarvanMo

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

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

正文

今天我们将看看Flutter的Navigation。

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

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

1_yptwp6Ahe_-yhrLTg-NqwQ.png

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


multiple-navigators-BottomNavigationBar-animation.gif

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

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

好了,让我们开始。

一切都关于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()方法是如何工作的?

你可能好奇 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));
        });
  }
}

这个怎么起作用的?

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

几点说明:

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

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

上一篇下一篇

猜你喜欢

热点阅读