
                            
                            因收到Google相关通知，网站将会择期关闭。相关通知内容
                            
                            
                            23 应用界面整合
                            1. 界面整合的需求分析

如下所示，在应用的底部添加导航栏，进行界面间的切换操作。下面从数据和界面的角度对该进行分析：




————————————————————
————————————————————








当前界面中需要添加的数据有:


底部栏的文字、图标资源列表
底部栏的激活索引


在点击底部栏的按钮时，需要更新激活索引，并进行界面的重新构建。这里定义一个 MenuData 类，用于维护标签和图标数据：

class MenuData {
  // 标签
  final String label;

  // 图标数据
  final IconData icon;

  const MenuData({
    required this.label,
    required this.icon,
  });
}




对于界面构建逻辑来说，这是一个上下结构，上面是内容区域，下面是底部导航栏。所以，可以通过 Column 组件上下排列，其中内容区域通过 Expanded 组件进行延展，内容组件根据激活的索引值构建不同的界面。





2. 代码实现：第一版

Flutter 中提供了 BottomNavigationBar 组件可以展示底部栏，这里单独封装一个 AppBottomBar 组件用于维护底部栏的界面构建逻辑。其中需要传入激活索引、点击回调、菜单数据列表：

class AppBottomBar extends StatelessWidget {
  final int currentIndex;
  final List<MenuData> menus;
  final ValueChanged<int>? onItemTap;

  const AppBottomBar({
    Key? key,
    this.onItemTap,
    this.currentIndex = 0,
    required this.menus,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      backgroundColor: Colors.white,
      onTap: onItemTap,
      currentIndex: currentIndex,
      elevation: 3,
      type: BottomNavigationBarType.fixed,
      iconSize: 22,
      selectedItemColor: Theme.of(context).primaryColor,
      selectedLabelStyle: const TextStyle(fontWeight: FontWeight.bold),
      showUnselectedLabels: true,
      showSelectedLabels: true,
      items: menus.map(_buildItemByMenuMeta).toList(),
    );
  }

  BottomNavigationBarItem _buildItemByMenuMeta(MenuData menu) {
    return BottomNavigationBarItem(
      label: menu.label,
      icon: Icon(menu.icon),
    );
  }
}




然后就是构建整体结构，这里创建一个 AppNavigation 组件来处理。由于激活索引数据需要在交互时改变，并重新构建界面，所以 AppNavigation 继承自 StatefulWidget，在状态类中处理界面构建和状态数据维护的逻辑。

class AppNavigation extends StatefulWidget {
  const AppNavigation({Key? key}) : super(key: key);

  @override
  State<AppNavigation> createState() => _AppNavigationState();
}

class _AppNavigationState extends State<AppNavigation> {
  int _index = 0;

  final List<MenuData> menus = const [
    MenuData(label: '猜数字', icon: Icons.question_mark),
    MenuData(label: '电子木鱼', icon: Icons.my_library_music_outlined),
    MenuData(label: '白板绘制', icon: Icons.palette_outlined),
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded( child: _buildContent(_index)),
        AppBottomBar(
          currentIndex: _index,
          onItemTap: _onChangePage,
          menus: menus,
        )
      ],
    );
  }

  void _onChangePage(int index) {
    setState(() {
      _index = index;
    });
  }


内容区域的构建使用 _buildContent 方法，根据不同的激活索引，返回创建不同的界面：


index = 0 时，构建猜数字界面；
index = 1 时，构建电子木鱼界面；
index = 2 时，构建白板绘制界面；


  Widget _buildContent(int index) {
    switch(index){
      case 0:
       return const GuessPage();
      case 1:
        return const MuyuPage();
      case 2:
        return const Paper();
      default:
        return const SizedBox.shrink();
    }
  }
}


到这里就完成了点击底部导航，切换界面的功能，当前代码位置: navigation 。 但这种方式处理会有一些问题：伴随着界面的消失，状态类会被销毁；下次再到该界面时会重新初始化状态类，如下所示：




在绘制面板绘制
切换后，状态重置













3. 状态类数据的保持

想要避免每次切换都会重置状态数据，大体上有三种解决方案：


1.使用 AutomaticKeepAliveClientMixin 对状态类进行保活，这种方案只能用于可滑动组件中。这里可以使用 PageView 组件来实现切页并保活的效果。
2.将状态数据提升到上层，比如将三个界面的状态数据都交由 _AppNavigationState 状态类维护。如果直接用这种方式，很容易造成一个超级大的类，来维护很多数据。其实状态管理工具，就是基于这种思路，将数据交由上层维护，同时提供了分模块处理数据的能力。
3.保持数据的持久性，比如将数据保存到本地文件或数据库，每次初始化时进行加载复现。这种方式处理起来比较麻烦，初始化加载数据也需要一点时间。但这种方式在界面不可见时，可以释放内存中的数据。




这里使用 方式 1 来处理是最简单的。在 _buildContent 方法中返回 PageView 组件，并将三个内容界面作为 children 入参，通过 PageController 来控制界面的切换。注意一点：将 physics 设置设置为 NeverScrollableScrollPhysics 可以禁止 PageView 的滑动，如果想要运行滑动切页，可以去除。

---->[_AppNavigationState]----
final PageController _ctrl = PageController();

Widget _buildContent() {
  return PageView(
    physics: const NeverScrollableScrollPhysics(),
    controller: _ctrl,
    children: const [
       GuessPage(),
       MuyuPage(),
       Paper(),
    ],
  );
}

void _onChangePage(int index) {
  _ctrl.jumpToPage(index);
  setState(() {
    _index = index;
  });
}


另外如果期望某个状态类保活，需要让其混入 AutomaticKeepAliveClientMixin, 并覆写 wantKeepAlive 返回 true 。如下是对画板状态类的处理，其他两个同理：






在绘制面板绘制
切换后，状态保活










到这里，就将之前的三个小案例，集成到了一个应用中，并且在切换界面的过程中，可以保持状态数据不被重置。当前代码位置 navigation。

上面可以保证程序运行期间，各界面状态类的保活，但是当应用关闭之后，内存中的数据会被清空。再次进入应用时还是无法恢复到之前的状态，想要记住用户的信息，就必须对数据进行持久化的存储。比如存储为本地文件、数据库、网络数据等，下一篇将介绍数据的持久化存储。



4. 优化一些缺陷

如下所示，左侧是 Column 组件上下排列，当键盘顶起之后，底部会留出一块空白，高为底部导航高度。想解决这个问题，使用 Scaffold 组件即可，它有一个 bottomNavigationBar 的插槽，不会被键盘顶起。




Column 结构
Scaffold 结构










这时，将 _AppNavigationState 的构建方法改为如下代码：

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: _buildContent(),
     bottomNavigationBar: AppBottomBar(
       currentIndex: _index,
       onItemTap: _onChangePage,
       menus: menus,
     ),
   );
 }




下面以 AppBar 的主题介绍一下 Flutter 默认配置的能力。项目中希望所有的 AppBar 都是白色背景、状态类透明、标题居中、图标颜色、文字颜色为黑色。



如果每次使用 AppBar 组件就配置一次，那代码书写将会非常复杂。Flutter 在主题数据的功能，只要指定主题，其下节点中的对应组件，就会默认使用的配置数据。如下所示，在 MaterialApp 的 theme 入参中可以配置主题数据：



这样，以前使用 AppBar 的地方就不用再配置那么多信息了。比如电子木鱼界面的 AppBar 就可以清爽多了：



这里只是拿 AppBarTheme 举个例子，还有其他很多的主题可以配置，大家可以在以后慢慢了解。



5. 本章小结

本章我们主要将之前的三个小案例整合到了一个项目中，通过底部导航栏 + PageView 实现界面间的切换。另外也就 State 的状态保活进行了简单地认识，这里只是程序运行期间，保证各界面状态类的活性，但是当应用关闭之后，内存中的数据会被清空。再次进入应用时还是无法恢复到之前的状态。

想要永久记住用户的信息，就必须对数据进行持久化的存储。比如存储为本地文件、数据库、网络数据等，在程序启动时进行加载，恢复状态数据。这是应用程序非常重要的一个部分，下一篇将介绍数据的持久化存储。

                        
                        
                            