Flutter for OpenHarmony年味+实用实战应用|搭建【多 Tab 应用】基础工程 + 实现【弧形底部导航】
本文介绍了如何在Flutter应用中集成curved_navigation_bar和font_awesome_flutter两个开源库,实现跨平台(包括鸿蒙系统)的弧形底部导航栏功能。主要内容包括:1)两个库的核心价值与适用场景;2)环境适配要求与多端支持情况;3)详细集成步骤,从pubspec.yaml配置到核心API调用示例;4)提供了一个完整的春节主题导航栏实现案例,包含3个Tab页面的状态
一、三方库核心价值与适用场景
1.1 本实战用到的两个库
| 库名 | 核心价值 | 适用场景 |
|---|---|---|
| curved_navigation_bar | 提供弧形凸起、带动画的底部导航栏,比系统默认 TabBar 更有辨识度 | 需要 3~5 个主 Tab、希望底部栏有强视觉差异的 App(如节日/品牌主题) |
| font_awesome_flutter | 提供 2000+ 矢量图标,风格统一,无需切图 | 需要丰富图标且希望与 Material/Cupertino 区分;多端图标一致 |
1.2 整体流程概览
二、环境适配
2.1 支持的 Flutter 版本
| 库 | 最低 Flutter / Dart | 本教程验证 |
|---|---|---|
| curved_navigation_bar ^1.0.6 | Dart 2.12+ | Flutter 3.x + Dart 3.x ✅ |
| font_awesome_flutter ^10.12.0 | 无特殊要求 | Flutter 3.35+(含鸿蒙 canary)✅ |
2.2 多端适配情况
- curved_navigation_bar:纯 Flutter 绘制,Android / iOS / 鸿蒙 / Web / 桌面均可使用,无需平台通道。
- font_awesome_flutter:字体资源随包体下发,鸿蒙端与 Android/iOS 行为一致。
- 鸿蒙注意点:需使用支持Flutter for OpenHarmony最新适配分支版本,且完成鸿蒙工程配置与签名后再打包 HAP。
三、集成步骤
3.1 第一步:pubspec.yaml 配置
在项目根目录的 pubspec.yaml 的 dependencies: 下增加两行。
为什么用 ^1.0.6 / ^10.12.0:^ 表示兼容该主版本下的次版本更新,既保证可用又便于后续安全升级。
Flutter 端代码(项目根目录 pubspec.yaml 片段):
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
curved_navigation_bar: ^1.0.6 # 弧形底部导航
font_awesome_flutter: ^10.12.0 # 图标库,与导航栏搭配使用
3.2 第二步:依赖安装
在项目根目录执行:
flutter pub get
看到 Got dependencies! 即表示成功。为什么必须执行:Flutter 不会自动把 pubspec.yaml 的变更同步到 .dart_tool/package_config.json,不执行则 import 会报「Target of URI doesn’t exist」。
3.3 第三步:基础初始化(无额外初始化)
这两个库不需要在 main() 或 runApp 前做类似 WidgetsFlutterBinding.ensureInitialized() 之外的初始化,直接在使用页面 import 即可。
Flutter 端代码(在需要使用的 dart 文件顶部):
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
3.4 第四步:核心 API 调用示例
下面为项目中 lib/main.dart 的完整实现:带 3 个 Tab(春节祝福、绿色烟花、宅家拜年)的弧形底部栏,每 Tab 对应独立页面。
为什么用 IndexedStack:保留各 Tab 页面状态,避免每次切换重建;若希望每次进入都刷新,可改为按 index 只 build 当前页。
为什么用 late final List<_TabItem> _tabs:列表内包含 Widget(如 _BlessingPage()),不是 const,故不能用 const List,用 late final 只初始化一次。
Flutter 端代码(与项目 lib/main.dart 一致):
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '新春祝福',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFC41E3A),
brightness: Brightness.light,
primary: const Color(0xFFC41E3A),
secondary: const Color(0xFFD4AF37),
),
useMaterial3: true,
),
home: const MainNavigationPage(),
);
}
}
class MainNavigationPage extends StatefulWidget {
const MainNavigationPage({super.key});
State<MainNavigationPage> createState() => _MainNavigationPageState();
}
class _MainNavigationPageState extends State<MainNavigationPage> {
int _currentIndex = 0;
late final List<_TabItem> _tabs = [
_TabItem(
label: '春节祝福',
icon: FontAwesomeIcons.gift,
page: _BlessingPage(),
),
_TabItem(
label: '绿色烟花',
icon: FontAwesomeIcons.wandMagicSparkles,
page: _FireworkPage(),
),
_TabItem(
label: '宅家拜年',
icon: FontAwesomeIcons.house,
page: _HomeGreetingPage(),
),
];
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: IndexedStack(
index: _currentIndex,
children: _tabs.map((e) => e.page).toList(),
),
bottomNavigationBar: CurvedNavigationBar(
index: _currentIndex,
height: 60,
backgroundColor: Colors.transparent,
color: const Color(0xFFC41E3A),
buttonBackgroundColor: const Color(0xFFD4AF37),
animationCurve: Curves.easeInOutCubic,
animationDuration: const Duration(milliseconds: 400),
onTap: (index) => setState(() => _currentIndex = index),
items: _tabs
.map(
(tab) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: FaIcon(
tab.icon,
color: _currentIndex == _tabs.indexOf(tab)
? const Color(0xFFC41E3A)
: Colors.white,
size: 26,
),
),
)
.toList(),
),
);
}
}
class _TabItem {
final String label;
final IconData icon;
final Widget page;
const _TabItem({
required this.label,
required this.icon,
required this.page,
});
}
class _BlessingPage extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFFFF8F0),
Color(0xFFFFE4D6),
],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.gift,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'春节祝福',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: const Color(0xFFC41E3A),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'新春快乐,阖家幸福\n万事如意,福寿安康',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF5C4033),
height: 1.8,
),
),
),
],
),
),
),
);
}
}
class _FireworkPage extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFE8F5E9),
Color(0xFFC8E6C9),
],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.wandMagicSparkles,
size: 64,
color: const Color(0xFF2E7D32),
),
const SizedBox(height: 24),
Text(
'绿色烟花',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: const Color(0xFF1B5E20),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'环保过节,绿色迎新\n少放烟花,清新空气',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF33691E),
height: 1.8,
),
),
),
],
),
),
),
);
}
}
class _HomeGreetingPage extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Color(0xFFFFFDE7),
Color(0xFFFFECB3),
],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.house,
size: 64,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(height: 24),
Text(
'宅家拜年',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: const Color(0xFFB8860B),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'视频拜年,心意不减\n平安健康,就是福气',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF6D4C00),
height: 1.8,
),
),
),
],
),
),
),
);
}
}

四、高级封装
4.1 为什么要封装
- 多个入口(如首页、某模块)需要同一套底部栏时,避免复制粘贴。
- 主题(颜色、高度、动画)集中管理,换肤或多端微调只改一处。
- 使用「统一调用入口」后,业务侧只关心「传哪些 Tab」,不关心内部实现。
4.2 封装结构
- CurvedTabItem:单个 Tab 的配置(label、icon、page)。
- CurvedBarTheme:导航栏颜色、高度、动画等。
- AppCurvedBottomBar:内部使用
CurvedNavigationBar+FaIcon,根据items和theme渲染。 - CurvedBottomBarConfig:单例风格统一入口,提供默认主题和
build()方法。
4.3 可复用封装代码
4.3.1 数据与主题类(lib/widgets/curved_bottom_bar.dart 内)
Flutter 端代码:
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
/// 📦 单个选项卡配置(用于二次封装)
class CurvedTabItem {
final String label;
final IconData icon;
final Widget page;
const CurvedTabItem({
required this.label,
required this.icon,
required this.page,
});
}
/// 🎨 弧形底部导航栏主题配置
class CurvedBarTheme {
final Color barColor;
final Color buttonBackgroundColor;
final Color selectedIconColor;
final Color unselectedIconColor;
final double height;
final Duration animationDuration;
final Curve animationCurve;
const CurvedBarTheme({
this.barColor = const Color(0xFFC41E3A),
this.buttonBackgroundColor = const Color(0xFFD4AF37),
this.selectedIconColor = const Color(0xFFC41E3A),
this.unselectedIconColor = Colors.white,
this.height = 60,
this.animationDuration = const Duration(milliseconds: 400),
this.animationCurve = Curves.easeInOutCubic,
});
}
4.3.2 组件类(同上文件内,接在 CurvedBarTheme 后)
Flutter 端代码:
/// 🧩 二次封装:带 Font Awesome 图标的弧形底部导航栏
///
/// 将 [CurvedNavigationBar] 与 [FaIcon] 结合,统一管理选项卡与页面切换。
class AppCurvedBottomBar extends StatefulWidget {
final List<CurvedTabItem> items;
final CurvedBarTheme? theme;
const AppCurvedBottomBar({
super.key,
required this.items,
this.theme,
});
State<AppCurvedBottomBar> createState() => _AppCurvedBottomBarState();
}
class _AppCurvedBottomBarState extends State<AppCurvedBottomBar> {
int _currentIndex = 0;
late CurvedBarTheme _theme;
void initState() {
super.initState();
_theme = widget.theme ?? const CurvedBarTheme();
}
void didUpdateWidget(covariant AppCurvedBottomBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.theme != null) _theme = widget.theme!;
}
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: IndexedStack(
index: _currentIndex,
children: widget.items.map((e) => e.page).toList(),
),
bottomNavigationBar: CurvedNavigationBar(
index: _currentIndex,
height: _theme.height,
backgroundColor: Colors.transparent,
color: _theme.barColor,
buttonBackgroundColor: _theme.buttonBackgroundColor,
animationCurve: _theme.animationCurve,
animationDuration: _theme.animationDuration,
onTap: (index) => setState(() => _currentIndex = index),
items: widget.items
.asMap()
.entries
.map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: FaIcon(
entry.value.icon,
color: _currentIndex == entry.key
? _theme.selectedIconColor
: _theme.unselectedIconColor,
size: 26,
),
),
)
.toList(),
),
);
}
}
4.3.3 统一调用入口(单例风格,lib/widgets/curved_bottom_bar_config.dart)
为什么用单例:全局只有一份「默认主题 + 构建方法」,避免多处写死颜色或高度,鸿蒙/Android/iOS 可共用,后续若要按平台区分主题只需改这一处。
Flutter 端代码:
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'curved_bottom_bar.dart';
/// 🎯 统一调用入口:提供默认主题与快捷构建方法,避免各处散落配置。
///
/// 使用单例风格集中管理「默认主题 + 创建底部栏」,便于全局换肤与多端一致体验。
class CurvedBottomBarConfig {
CurvedBottomBarConfig._();
static final CurvedBottomBarConfig _instance = CurvedBottomBarConfig._();
/// 单例入口,保证全局唯一配置源。
static CurvedBottomBarConfig get instance => _instance;
/// 默认主题(鸿蒙/Android/iOS 共用一套,也可按 Platform 分支)。
static CurvedBarTheme get defaultTheme => const CurvedBarTheme(
barColor: Color(0xFFC41E3A),
buttonBackgroundColor: Color(0xFFD4AF37),
selectedIconColor: Color(0xFFC41E3A),
unselectedIconColor: Colors.white,
height: 60,
animationDuration: Duration(milliseconds: 400),
animationCurve: Curves.easeInOutCubic,
);
/// 统一创建底部导航页:传入选项卡列表,可选覆盖主题。
/// 为什么这么做:业务侧只需关心「有哪些 tab」,样式走统一入口,便于维护与多端一致。
static Widget build({
required List<CurvedTabItem> items,
CurvedBarTheme? theme,
}) {
return AppCurvedBottomBar(
items: items,
theme: theme ?? defaultTheme,
);
}
/// 示例:预置春节主题的选项卡配置(仅作参考,实际页面由调用方传入)。
static List<CurvedTabItem> sampleItems({
required Widget blessingPage,
required Widget fireworkPage,
required Widget homePage,
}) {
return [
CurvedTabItem(
label: '春节祝福',
icon: FontAwesomeIcons.gift,
page: blessingPage,
),
CurvedTabItem(
label: '绿色烟花',
icon: FontAwesomeIcons.wandMagicSparkles,
page: fireworkPage,
),
CurvedTabItem(
label: '宅家拜年',
icon: FontAwesomeIcons.house,
page: homePage,
),
];
}
}
4.3.4 业务侧使用统一入口
本项目 lib/main.dart 当前采用「直接使用 CurvedNavigationBar」方式(见上文 3.4)。若改用封装入口,可如下使用;页面需为可对外引用的 Widget(如将 _BlessingPage 改为 BlessingPage 并放到单独文件,或直接传入本文件内定义的页面实例)。
Flutter 端代码:
// 在 main.dart 或首页
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:spark_bless/widgets/curved_bottom_bar.dart';
import 'package:spark_bless/widgets/curved_bottom_bar_config.dart';
// 方式一:用统一入口构建,不传 theme 即用默认主题
home: CurvedBottomBarConfig.build(
items: CurvedBottomBarConfig.sampleItems(
blessingPage: BlessingPage(),
fireworkPage: FireworkPage(),
homePage: HomeGreetingPage(),
),
),
// 方式二:自定义 items + 自定义主题
home: CurvedBottomBarConfig.build(
items: [
CurvedTabItem(label: '春节祝福', icon: FontAwesomeIcons.gift, page: BlessingPage()),
CurvedTabItem(label: '绿色烟花', icon: FontAwesomeIcons.wandMagicSparkles, page: FireworkPage()),
CurvedTabItem(label: '宅家拜年', icon: FontAwesomeIcons.house, page: HomeGreetingPage()),
],
theme: CurvedBarTheme(
barColor: Colors.deepPurple,
buttonBackgroundColor: Colors.amber,
height: 65,
),
),
五、工程优化
5.1 体积优化
- font_awesome_flutter:包内带字体文件,会增大体积。若只需少量图标,可考虑按需裁剪或换用仅含常用图标的包(若有);当前版本暂无官方 tree-shake 图标方案,可关注后续更新。
- 鸿蒙 HAP:使用
flutter build hap --release并开启混淆(如--obfuscate)可减小体积;同时检查是否引入未使用的 native 插件。
5.2 性能优化
- IndexedStack:会一次性 build 所有子页,若 Tab 较多或单页很重,可改为按
_currentIndex只 build 当前页,牺牲「保留状态」换内存与首帧时间。 - CurvedNavigationBar:动画时长不宜过长(建议 300~500ms),避免低端机卡顿。
- 主题:颜色与尺寸尽量通过
CurvedBarTheme统一设置,避免在onTap或 build 里重复创建Color/Duration对象。
六、高频问题
6.1 集成报错
| 报错/现象 | 原因 | 解决 |
|---|---|---|
Target of URI doesn't exist: 'package:curved_navigation_bar/...' |
依赖未拉取或 IDE 未刷新 | 执行 flutter pub get;必要时 Flutter: Restart Analysis Server |
Invalid constant value / isn't a const constructor |
在 const 列表中放了 Widget 等非常量 | 将 const List<...> 改为 late final List<...> _tabs = [...]; 或去掉 const |
Member not found: 'houseChimneyHeart' |
当前 font_awesome_flutter 版本无该图标 | 改用 FontAwesomeIcons.house 等已存在的图标名 |
6.2 API 使用误区
- index 与 onTap 不同步:务必用「单一数据源」——一个
_currentIndex,既传给CurvedNavigationBar的index,又在onTap里setState更新,否则会出现选中与页面错位。 - IndexedStack 的 children 顺序:必须与
items一一对应且顺序一致,否则点击第 N 个 tab 会显示错误的页面。 - extendBody: true:建议设为
true,否则底部易留白或与导航栏割裂;若页面内容需要避开安全区,用SafeArea包裹 body 内容。
6.3 性能问题
- 若切换 Tab 时明显卡顿:检查当前 Tab 页是否在 build 里做了重计算或大列表无懒加载;可先改为「只 build 当前页」验证是否与 IndexedStack 有关。
- 若首屏白屏时间长:检查是否在首页同步做了大量初始化;可把非必要初始化延后或放到 isolate。
更多推荐

所有评论(0)