大多数 App 中都会有底部导航栏,通过底部导航栏切换实现不同页面之间的切换。在Flutter 中提供了 BottomNavigationBar组件实现底部导航。本篇介绍通过 BottomNavigationBarIndexedStack构建最为常见的 App 页面框架。

image.png
最终实现的结果如上图所示,顶部共用一个导航栏,底部有四个图标导航,点击对应的图标跳转到对应的页面。

图标准备

本次例程需要4个图标,2种颜色,可以从 iconfont 中找到自己需要的图标下载不同的颜色使用。然后在 pubspec.yaml 中的 assets 指定素材所在目录。需要注意的是如果是 png 文件直接指定整个目录即可,但如果是 jpg 格式,则需要同时指定文件名及后缀。

BottomNavigationBar 简介

BottomNavigationBar的构造函数如下:

  1. BottomNavigationBar({
  2. Key? key,
  3. required this.items,
  4. this.onTap,
  5. this.currentIndex = 0,
  6. this.elevation,
  7. this.type,
  8. Color? fixedColor,
  9. this.backgroundColor,
  10. this.iconSize = 24.0,
  11. Color? selectedItemColor,
  12. this.unselectedItemColor,
  13. this.selectedIconTheme,
  14. this.unselectedIconTheme,
  15. this.selectedFontSize = 14.0,
  16. this.unselectedFontSize = 12.0,
  17. this.selectedLabelStyle,
  18. this.unselectedLabelStyle,
  19. this.showSelectedLabels,
  20. this.showUnselectedLabels,
  21. this.mouseCursor,
  22. })

其中常用的属性为:

  • items:及对应的页面组件数组
  • currentIndex:默认显示第几个页面
  • type:组件类型,使用BottomNavigationBarType枚举,有 fixedshifting 两种。fixed 是图标固定位置,而 shifting 的图标点击后会有一个漂移效果,可以实际试一下,一般用fixed 比较多。
  • onTap:点击后的事件,一般用这个更新状态数据,以便更新页面。

其他属性用于控制样式的,可以根据实际需要设置图标大小,主题色,字体等参数。

构建项目页面结构

首先,新建四个业务页面,分别是 dynamic.dartmessage.dartcategory.dartmine.dart,分别对应动态、消息、分类浏览和个人中心四个页面。目前这四个页面很简单,只是在页面中间依次显示“岛上码农”四个字。代码都是类似的,以 dynamic 为例:

  1. import 'package:flutter/material.dart';
  2. class DynamicPage extends StatelessWidget {
  3. const DynamicPage({Key key}) : super(key: key);
  4. @override
  5. Widget build(BuildContext context) {
  6. return Scaffold(
  7. body: Center(
  8. child: Text('岛'),
  9. ),
  10. );
  11. }
  12. }

注意的是这里的 Scaffold 没有 AppBar 了,这是因为在首页已经有了,如果再有 AppBar 就会出现两个。

其次,新建首页,用于管理四个业务页面,命名为 app.dartapp.dart 使用了 BottomNavigationBar 管理四个业务页面的切换。

  1. import 'package:flutter/material.dart';
  2. import 'dynamic.dart';
  3. import 'message.dart';
  4. import 'category.dart';
  5. import 'mine.dart';
  6. class AppHomePage extends StatefulWidget {
  7. AppHomePage({Key key}) : super(key: key);
  8. @override
  9. _AppHomePageState createState() => _AppHomePageState();
  10. }
  11. class _AppHomePageState extends State<AppHomePage> {
  12. int _index = 0;
  13. List<Widget> _homeWidgets = [
  14. DynamicPage(),
  15. MessagePage(),
  16. CategoryPage(),
  17. MinePage(),
  18. ];
  19. void _onBottomNagigationBarTapped(index) {
  20. setState(() {
  21. _index = index;
  22. });
  23. }
  24. @override
  25. Widget build(BuildContext context) {
  26. return Scaffold(
  27. appBar: AppBar(
  28. title: Text('岛上码农'),
  29. ),
  30. body: IndexedStack(
  31. index: _index,
  32. children: _homeWidgets,
  33. ),
  34. bottomNavigationBar: BottomNavigationBar(
  35. type: BottomNavigationBarType.fixed,
  36. currentIndex: _index,
  37. onTap: _onBottomNagigationBarTapped,
  38. items: [
  39. _getBottomNavItem(
  40. '动态', 'images/dynamic.png', 'images/dynamic-hover.png', 0),
  41. _getBottomNavItem(
  42. ' 消息', 'images/message.png', 'images/message-hover.png', 1),
  43. _getBottomNavItem(
  44. '分类浏览', 'images/category.png', 'images/category-hover.png', 2),
  45. _getBottomNavItem(
  46. '个人中心', 'images/mine.png', 'images/mine-hover.png', 3),
  47. ],
  48. ),
  49. );
  50. }
  51. BottomNavigationBarItem _getBottomNavItem(
  52. String title, String normalIcon, String pressedIcon, int index) {
  53. return BottomNavigationBarItem(
  54. icon: _index == index
  55. ? Image.asset(
  56. pressedIcon,
  57. width: 32,
  58. height: 28,
  59. )
  60. : Image.asset(
  61. normalIcon,
  62. width: 32,
  63. height: 28,
  64. ),
  65. label: title,
  66. );
  67. }
  68. }

这里关键的地方有两个,一是使用的 IndexedStack,这是一个管理页面显示层级的容器。使用 index 属性确定当前容器里那个页面在最顶上,容器里的页面通过 children 属性设置,要求是一个 Widget 数组。因此,逻辑就是当 BottomNavigationBar 中的图标被点击后,对应点击事件会回调 onTap属性指定的方法,将当前的点击索引值传递回调函数,因此可以利用这个方式控制 IndexedStack 的页面层级切换。

最后,使用了状态变量_index 存储IndexedStatck当前显示页面的索引值,然后当 BottomNavigationBar的图标点击事件发生后,在回调函数中使用 setState 更新状态变量_index 来刷新当前界面。

简化入口

main.dart 是入口文件,应当做最基础的配置和全局初始化配置,而不应该有业务代码,因此可以简化为从main 方法加载首页即可。通过这种方式可以让 main.dart 文件即为简洁。这也是在开发的时候需要注意的地方,将不相关的代码剥离,相关的代码聚合,即所谓的“高内聚,低耦合”原则。

  1. import 'package:flutter/material.dart';
  2. import 'app.dart';
  3. void main() {
  4. runApp(MyApp());
  5. }
  6. class MyApp extends StatelessWidget {
  7. @override
  8. Widget build(BuildContext context) {
  9. return MaterialApp(
  10. title: 'App 框架',
  11. theme: ThemeData(
  12. primarySwatch: Colors.blue,
  13. ),
  14. home: AppHomePage(),
  15. );
  16. }
  17. }

代码复用

写代码的时候要注意复用,在这里将构建 BottomNavigationBar 元素抽离出了一个构建方法_getBottomNavItem,从而提高代码的复用性和维护性,也可以避免 Flutter 的组件构建的 build 方法中出现过多的元素和嵌套,影响代码的可读性。