Flutter for OpenHarmony:从零搭建今日资讯App(三)主框架与底部导航
本文介绍了Flutter应用中实现底部导航主框架的最佳实践。通过使用NavigationBar组件和IndexedStack,实现了页面切换、状态保持、视觉反馈等功能。文章详细解析了代码实现,包括主框架UI构建、导航栏配置、图标选择原则等关键点,并解释了IndexedStack保持页面状态的原理。这种实现方式在保证用户体验的同时兼顾了性能优化,适合4-5个Tab的应用场景。

主框架是应用的骨架,包含底部导航栏和页面容器。核心功能是Tab切换和页面状态保持。做好底部导航需要考虑很多细节,比如状态保持、流畅动画、视觉反馈等。
底部导航功能需求
在写代码之前,先明确底部导航要实现的功能:
页面切换 - 点击不同的Tab显示不同的页面
状态保持 - 切换Tab后再切回来,之前的滚动位置、输入内容等状态应该保持
视觉反馈 - 当前选中的Tab要有明显的视觉区分
流畅动画 - 切换Tab时要有平滑的过渡
性能优化 - 不能因为有多个页面就占用太多内存
选择导航组件
Flutter提供了几种底部导航的实现方式:
BottomNavigationBar - Flutter早期的组件,稳定但样式老旧
NavigationBar - Material Design 3的新组件,样式现代,动画流畅
CupertinoTabBar - iOS风格的导航,适合iOS风格应用
我们选择NavigationBar,因为它代表了Flutter的未来方向,而且会自动适配深色模式。
创建主框架文件
在lib/screens目录下创建main_screen.dart:
import 'package:flutter/material.dart';
import 'home/home_screen.dart';
import 'categories/categories_screen.dart';
import 'favorites/favorites_screen.dart';
import 'profile/profile_screen.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
State<MainScreen> createState() => _MainScreenState();
}
代码解析:
- 导入四个页面:首页、分类、收藏、个人中心
- 使用
StatefulWidget管理当前选中的Tab索引 - 为什么叫main_screen而不是bottom_navigation?因为这个文件不只是底部导航,它是整个应用的主框架
定义状态和页面列表
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const CategoriesScreen(),
const FavoritesScreen(),
const ProfileScreen(),
];
代码解析:
_currentIndex- 当前选中的Tab索引,0表示首页,1表示分类,以此类推_screens- 四个页面的列表,通过索引访问- 使用
const构造函数优化性能,Flutter会复用Widget实例 - 变量名前面的下划线表示这是私有变量,只能在这个文件内访问
页面顺序很重要:List的顺序要和底部导航按钮的顺序一致,否则点击按钮会跳到错误的页面。
构建主框架UI
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _screens,
),
代码解析:
Scaffold- 提供Material Design的基本布局结构IndexedStack- 关键组件,同时创建所有子Widget但只显示指定index的那个- 其他Widget虽然不可见但依然存在,状态得以保持
- 切换Tab时页面的滚动位置、输入内容等不会丢失
为什么不用_screens[_currentIndex]:
如果直接用索引访问,每次切换Tab都会销毁旧页面、创建新页面,页面状态就丢失了。用户在首页浏览新闻,看到第10条时切换到收藏Tab,然后又切回首页,如果页面重新加载,又要从第1条开始看,用户会很烦。
IndexedStack虽然占用更多内存(同时创建4个页面),但换来的是更好的用户体验。对于4-5个Tab的应用,这点内存开销是值得的。
实现底部导航栏
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() {
_currentIndex = index;
});
},
代码解析:
NavigationBar- Material Design 3的底部导航组件selectedIndex- 当前选中的Tab,与IndexedStack的index保持一致onDestinationSelected- 点击Tab时触发,参数index就是被点击的Tab索引setState- 通知Flutter重新执行build方法,更新UI
setState的作用:这是Flutter状态管理的基础。调用setState告诉Flutter:“我的状态变了,请重新执行build方法”。如果不用setState包裹,_currentIndex虽然会改变,但界面不会更新。
配置导航按钮
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '首页',
),
NavigationDestination(
icon: Icon(Icons.category_outlined),
selectedIcon: Icon(Icons.category),
label: '分类',
),
NavigationDestination(
icon: Icon(Icons.favorite_outline),
selectedIcon: Icon(Icons.favorite),
label: '收藏',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
代码解析:
icon- 未选中时显示的图标(轮廓风格)selectedIcon- 选中时显示的图标(填充风格)label- 图标下方的文字,简短有力,最好两个字- 图标选择遵循Material Design规范,用户一看就懂
图标的选择原则:
- 首页用房子图标(home)
- 分类用分类图标(category)
- 收藏用心形图标(favorite)
- 个人中心用人形图标(person)
这些都是通用的设计语言,用户看到就知道是什么功能。
完整代码
完整的main_screen.dart文件:
import 'package:flutter/material.dart';
import 'home/home_screen.dart';
import 'categories/categories_screen.dart';
import 'favorites/favorites_screen.dart';
import 'profile/profile_screen.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const CategoriesScreen(),
const FavoritesScreen(),
const ProfileScreen(),
];
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _screens,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() {
_currentIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '首页',
),
NavigationDestination(
icon: Icon(Icons.category_outlined),
selectedIcon: Icon(Icons.category),
label: '分类',
),
NavigationDestination(
icon: Icon(Icons.favorite_outline),
selectedIcon: Icon(Icons.favorite),
label: '收藏',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
代码不到70行,但实现了完整的底部导航功能。代码简洁、逻辑清晰、易于维护。
创建临时页面
为了让应用能运行,需要创建四个临时页面。以首页为例:
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: const Center(child: Text('首页内容')),
);
}
}
代码解析:
- 最简单的页面实现,只显示标题和文字
- 足够测试底部导航是否正常工作
- 后续会逐步完善这些页面
用同样方式创建其他三个页面(CategoriesScreen、FavoritesScreen、ProfileScreen),只需要改改类名和显示的文字。
运行测试
flutter run
测试要点:
- 启动页显示2-3秒后自动跳转
- 底部显示四个导航按钮
- 点击不同按钮切换页面
- 切换后再切回来,页面状态保持
试着在首页滚动一下(虽然现在还没内容可滚动),然后切换到其他Tab,再切回首页,你会发现页面状态保持了。这就是IndexedStack的作用。
进阶优化
双击Tab回到顶部
有些应用支持双击当前Tab回到顶部,这是个不错的功能:
onDestinationSelected: (index) {
if (index == _currentIndex) {
// 双击当前Tab,滚动到顶部
_scrollToTop(index);
} else {
setState(() {
_currentIndex = index;
});
}
},
具体的滚动实现要在各个页面里处理,这里只是提供一个思路。
添加角标提示
有时候需要在Tab上显示未读消息数量:
NavigationDestination(
icon: Stack(
children: [
Icon(Icons.favorite_outline),
if (unreadCount > 0)
Positioned(
right: 0,
top: 0,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
unreadCount > 99 ? '99+' : '$unreadCount',
style: TextStyle(fontSize: 10, color: Colors.white),
),
),
),
],
),
label: '收藏',
)
代码解析:
- 使用
Stack叠加图标和角标 - 红色圆点显示未读数量
- 数量超过99显示"99+",避免数字太长
处理返回键
从主页面跳转到详情页时,底部导航栏不应该显示。这是自动的,因为用Navigator.push会在当前页面上叠加新页面,新页面有自己的Scaffold。
如果想在主页面按返回键时退出应用:
WillPopScope(
onWillPop: () async {
return true; // 允许返回,退出应用
},
child: Scaffold(...),
)
设计原则
做底部导航时要遵循的原则:
Tab数量 - 3-5个最佳,太少功能不够,太多用户记不住
图标选择 - 直观易懂,使用用户熟悉的图标
文字标签 - 虽然图标很直观,但文字标签也不能省
颜色使用 - 使用主题颜色,自动适配深色模式
性能考虑 - IndexedStack同时创建4个页面,内存开销可接受
状态保持的重要性
用户在首页浏览新闻,看到第10条时切换到收藏Tab,然后又切回首页。如果页面重新加载,又要从第1条开始看,用户会很烦。所以状态保持不是可选项,而是必须项。
IndexedStack会同时创建所有页面,占用更多内存。但换来的是更好的用户体验。这是个典型的空间换时间的例子。在移动设备上,内存是宝贵的资源,但对于4-5个Tab的应用,这点内存开销是值得的。
简单就是美
我见过有些应用,底部导航做得很复杂,各种动画、特效。但用户要的不是炫酷的效果,而是快速、流畅的切换。所以建议不要过度设计,用Flutter提供的标准组件,遵循Material Design规范,就能做出很好的效果。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里你可以找到更多Flutter开发资源,与其他开发者交流经验,共同进步。
更多推荐



所有评论(0)