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 的状态保活进行了简单地认识,这里只是程序运行期间,保证各界面状态类的活性,但是当应用关闭之后,内存中的数据会被清空。再次进入应用时还是无法恢复到之前的状态。
想要永久记住用户的信息,就必须对数据进行持久化的存储。比如存储为本地文件、数据库、网络数据等,在程序启动时进行加载,恢复状态数据。这是应用程序非常重要的一个部分,下一篇将介绍数据的持久化存储。