【开源鸿蒙跨平台开发先锋训练营】DAY8~DAY13 底部选项卡&动态功能实现
📚 目录
[【开源鸿蒙跨平台开发先锋训练营】DAY8~DAY13 底部选项卡&动态功能实现](#【开源鸿蒙跨平台开发先锋训练营】DAY8~DAY13 底部选项卡&动态功能实现)
[摘 要](#摘 要)
[1 概述](#1 概述)
[1.1 开发背景](#1.1 开发背景)
[1.2 开发目标](#1.2 开发目标)
[1.3 核心技术栈](#1.3 核心技术栈)
[2 开发环境准备](#2 开发环境准备)
[2.1 环境配置](#2.1 环境配置)
[2.2 本地资源配置](#2.2 本地资源配置)
[2.3 动态模块目录结构](#2.3 动态模块目录结构)
[3 底部选项卡功能实现](#3 底部选项卡功能实现)
[3.1 底部选项卡设计](#3.1 底部选项卡设计)
[3.2 底部动态导航组件实现](#3.2 底部动态导航组件实现)
[3.3 项目入口配置](#3.3 项目入口配置)
[4 动态页面核心静态实现](#4 动态页面核心静态实现)
[4.1 动态主页面:Tab 容器](#4.1 动态主页面:Tab 容器)
[4.2 动态卡片组件](#4.2 动态卡片组件)
[4.3 动态 - 最热页面](#4.3 动态 - 最热页面)
[4.4 动态 - 最新页面](#4.4 动态 - 最新页面)
[4.5 数据模型](#4.5 数据模型)
[5 运行验证与效果展示](#5 运行验证与效果展示)
[5.1 鸿蒙设备运行](#5.1 鸿蒙设备运行)
[5.2 静态页面常见问题排查](#5.2 静态页面常见问题排查)
[6 代码提交](#6 代码提交)
[7 总结与拓展](#7 总结与拓展)
[7.1 总结](#7.1 总结)
[7.2 拓展方向](#7.2 拓展方向)
[8 参考资料](#8 参考资料)
摘 要
本次开源鸿蒙跨平台开发先锋训练营DAY8~DAY13阶段,以Flutter为核心跨平台技术栈,基于开源鸿蒙设备适配要求,聚焦底部选项卡实现与动态模块核心功能开发。通过 Flutter 跨平台技术,在之前已经完成底部导航栏(首页 / 美食 / 动态 / 推荐 / 我的)基础上完善。本次将重点实现动态页面的最热 / 最新两个 Tab 页的列表实现,集成TabBar+TabBarView的动态主页面开发,还完成了该页面与底部导航栏的对接集成,替换原有简单页面;此外,文章配套给出了代码使用中的资源替换、样式调整方法,以及下拉刷新、状态管理、图片缓存等后续优化建议,后续完善动态页面的标准化开发,保证项目代码风格的统一性,且适配鸿蒙系统开发要求。
1 概述
1.1 开发背景
承接前期底部选项卡(首页 / 美食 / 动态 / 推荐 / 我的)框架搭建,本次 DAY8~DAY13 聚焦动态模块核心落地:基于 Flutter 技术栈,完成动态选项卡下 "最热"、"最新" 两个子页面的静态布局实现,规范本地资源引用(如动态卡片图片、用户头像),为后续对接真实接口、添加交互功能(关注、点赞)奠定基础。
1.2 开发目标
✅ 完善底部选项卡 "动态" 模块配置,确保与 "最热 / 最新" 子页面正确联动;
✅ 实现动态主页面(FoodNotePage)的 Tab 布局,支持 "最热 / 最新" 页面平滑切换;
✅ 完成两个静态子页面的 UI 搭建:含用户头像、动态图片、发布时间、互动数据等元素;
✅ 适配本地资源(assets/images/food_note/下的头像 / 动态图),解决图片加载路径问题;
✅ 在开源鸿蒙模拟器验证静态页面展示效果,确保无布局错乱、资源缺失。
1.3 核心技术栈
⚡ 1. 跨平台框架:Flutter(兼容OpenHarmony)
🔧 2. UI 组件:Flutter 原生组件(TabBar、ListView、Card)+ 本地资源加载
📊 资源管理:pubspec.yaml本地图片声明与Image.asset加载
💻 开发工具:VS Code(代码编写)、DevEco Studio 6.0.0 Release(鸿蒙设备调试)
2 开发环境准备
2.1 环境配置
确保已满足以下环境要求(承接前期配置):
🚀 1. Flutter环境:安装 Flutter 3.27.5版本,配置flutter-OHOS-1.0.1(鸿蒙适配分支)分支(兼容 OpenHarmony);
📱 2. 鸿蒙环境:安装DevEco Studio 6.0.0 Release,配置 OpenHarmony SDK(API Version 20(6.0.0.47)),创建鸿蒙模拟器;
📂 3. 项目初始化:基于现有flutter_harmonyos工程,调整目录结构,安装依赖。
2.2 本地资源配置
👇 pubspec.yaml声明动态模块资源,需确认配置无误(避免图片加载失败):
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/images/foods/ # 美食卡片图片
- assets/images/food_show/ # 美食页相关图片
- assets/images/food_show/dynamic/ # 清单页动态图
- assets/images/food_show/star/ # 排行页达人头像
- assets/images/food_show/task/ # 任务中心图标
# 新增美食笔记(动态)模块的本地资源声明
- assets/images/food_note/ # 动态模块根资源
- assets/images/food_note/avatar/ # 用户头像资源
- assets/images/food_note/dynamic/ # 动态图片资源
🔧 配置后执行 flutter pub get,确保资源被 Flutter 工程识别。
2.3 动态模块目录结构
👇 在lib/pages/food_note/下完善目录,仅新增必要文件:
lib/
├── pages/
│ ├── food_note/ # 动态模块根目录
│ │ ├── food_note_page.dart # 动态主页面(Tab容器:最热+最新)
│ │ ├── tab/ # 两个子页面(静态)
│ │ │ ├── food_note_hot_tab.dart # 动态-最热页面(静态)
│ │ │ └── food_note_new_tab.dart # 动态-最新页面(静态)
│ │ ├── components/ # 动态模块专属组件
│ │ │ └── food_note_card.dart # 动态卡片(静态展示)
│ │ └── models/ # 复用数据模型(无需新增)
│ │ └── dynamic_model.dart # 动态数据模型(标题、图片、作者等)
├── components/ # 全局组件(复用)
│ ├── empty_widget.dart # 空数据占位(复用)
│ └── loading_widget.dart # 加载占位(复用)
└── utils/ # 工具类(复用)
└── date_util.dart # 时间格式化(复用)
3 底部选项卡功能实现
3.1 底部选项卡设计
✅ 1. 选项卡数量:5 个(首页、美食、动态、推荐、我的),覆盖核心服务场景;
✅ 2. 交互状态:默认状态(灰色图标 + 文字)、选中状态(蓝色高亮图标 + 文字),视觉区分明确;
✅ 3. 切换逻辑:平滑切换,保留各页面状态(如列表滚动位置、输入框内容),避免重复请求数据。
3.2 底部动态导航组件实现
🚀 动态选项卡关联
修改bottom_nav_bar.dart,确保底部导航的 "动态" 选项卡正确关联动态主页面(FoodNotePage),代码如下:
// components/ # 公共组件
// 底部导航栏:实现底部导航栏 bottom_nav_bar.dart
import 'package:flutter/material.dart';
// 1. 修复导入路径:去掉空格,用下划线,确保文件名匹配
import '../pages/home/home_page.dart';
import '../pages/food_show/food_show_page.dart';
import '../pages/food_note/food_note_page.dart';// 导入动态页面
import '../pages/eat_what/eat_what_page.dart';
import '../pages/mine/mine_page.dart';
// import 'package:flutter_harmonyos/models/food_model.dart';
class BottomNavBar extends StatefulWidget {
const BottomNavBar({super.key});
@override
State<BottomNavBar> createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
int _currentIndex = 0; // 默认选中首页
// 2. 去掉const,避免StatefulWidget构造函数不匹配
final List<Widget> _pages = [
const HomePage(), // 首页 // 如果HomePage构造函数是const,加const;否则去掉
const FoodShowPage(), // 美食
const FoodNotePage(), // 动态
const EatWhatPage(), // 推荐/搜索
const MinePage(), // 我的
];
// 3. 优化标签文字:标题+图标+样式全优化
final List<BottomNavigationBarItem> _navItems = const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'), // 🏠 房子图标
BottomNavigationBarItem(icon: Icon(Icons.food_bank), label: '美食'), // 🍽️ 食物图标
BottomNavigationBarItem(icon: Icon(Icons.note_alt), label: '动态'), // 📝 笔记图标 美食笔记
BottomNavigationBarItem(icon: Icon(Icons.restaurant), label: '推荐'), // 🍴 餐厅图标
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'), // 👤 人物图标
];
void _onTabTapped(int index) {
setState(() => _currentIndex = index);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
items: _navItems,
currentIndex: _currentIndex,
onTap: _onTabTapped,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
),
);
}
}
3.3 项目入口配置
项目入口配置(lib/main.dart)
将底部导航作为应用根页面,替换原有入口:
// 项目入口(重构)
import 'package:flutter/material.dart';
import 'components/bottom_nav_bar.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '本地美食清单',
theme: ThemeData(
primarySwatch: Colors.blue,
// visualDensity 是可选的适配优化,保留/删除都可以,不影响核心功能
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const BottomNavBar(), // 底部导航作为根页面
debugShowCheckedModeBanner: false,
);
}
}
4 动态页面核心静态实现
4.1 动态主页面:Tab 容器
💻 动态主页面(food_note_page.dart):Tab 容器
作为动态模块根页面,核心功能是实现 "最热 / 最新"Tab 切换,代码如下:
// food_note/ # 动态页面
// lib/pages/food_note/food_note_page.dart(动态)
import 'package:flutter/material.dart';
import 'package:flutter_harmonyos/pages/food_note/tab/food_note_hot_tab.dart';//导入最热页面
import 'package:flutter_harmonyos/pages/food_note/tab/food_note_new_tab.dart';//导入最新页面
class FoodNotePage extends StatefulWidget {
const FoodNotePage({super.key});
@override
State<FoodNotePage> createState() => _FoodNotePageState();
}
class _FoodNotePageState extends State<FoodNotePage> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
// 初始化Tab控制器(2个Tab:最热、最新)
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
// 销毁控制器,避免内存泄漏
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('动态'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: '最热'),
Tab(text: '最新'),
],
// 样式和美食页面保持一致
indicatorColor: Colors.red,
labelColor: Colors.red,
unselectedLabelColor: Colors.grey,
),
),
body: TabBarView(
controller: _tabController,
children: const [
FoodNoteHotTab(), // 最热Tab
FoodNoteNewTab(), // 最新Tab
],
),
);
}
}
4.2 动态卡片组件
📱 动态卡片组件(dynamic_card.dart):静态 UI
复用动态数据模型(DynamicModel),实现静态动态卡片(含头像、图片、互动数据),代码如下:
// lib/pages/food_note/components/ # 动态页面复用组件(如动态卡片)
// lib/pages/food_note/components/dynamic_card.dart # 动态卡片(本地图片版)
import 'package:flutter/material.dart';
import 'package:flutter_harmonyos/pages/food_note/models/dynamic_model.dart';
class DynamicCard extends StatelessWidget {
final DynamicModel dynamicModel; // 动态数据模型
const DynamicCard({super.key, required this.dynamicModel});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. 用户信息区
_buildUserInfo(),
const SizedBox(height: 12),
// 2. 内容图片区
_buildContentImages(),
const SizedBox(height: 8),
// 3. 内容文字区
_buildContentText(),
const SizedBox(height: 12),
// 4. 互动区(点赞/收藏/评论/分享)
_buildInteractionArea(),
// 5. 商品购买区(有商品才显示)
if (dynamicModel.productInfo != null) ...[
const SizedBox(height: 12),
_buildProductArea(),
],
],
),
),
);
}
// 用户信息(头像+用户名+发布时间+关注按钮)
Widget _buildUserInfo() {
return Row(
children: [
CircleAvatar(
// 修改1:NetworkImage → AssetImage(本地头像)
backgroundImage: AssetImage(dynamicModel.userAvatar),
radius: 20,
// 新增:头像加载失败时显示占位图标
onBackgroundImageError: (_, __) => const Icon(Icons.person, color: Colors.grey, size: 20),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dynamicModel.userName,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
Text(
dynamicModel.publishTime,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
const Spacer(),
// 关注按钮
ElevatedButton(
onPressed: () {}, // 后续可加关注逻辑
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: const Text('+ 关注', style: TextStyle(color: Colors.white, fontSize: 12)),
),
],
);
}
// 内容图片(单张/多张网格展示)
Widget _buildContentImages() {
if (dynamicModel.contentImages.isEmpty) return const SizedBox();
// 单张图片
if (dynamicModel.contentImages.length == 1) {
return Image.asset(
// 修改2:Image.network → Image.asset(本地内容图)
dynamicModel.contentImages.first,
height: 200,
width: double.infinity,
fit: BoxFit.cover,
// 新增:图片加载失败时显示占位图标
errorBuilder: (_, __, ___) => Container(
height: 200,
width: double.infinity,
color: Colors.grey[100],
child: const Icon(Icons.restaurant, color: Colors.grey, size: 40),
),
);
}
// 多张图片(3列网格)
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: dynamicModel.contentImages.length,
itemBuilder: (context, index) {
return Image.asset(
// 修改3:Image.network → Image.asset(本地内容图)
dynamicModel.contentImages[index],
fit: BoxFit.cover,
// 新增:网格图片加载失败时显示占位图标
errorBuilder: (_, __, ___) => Container(
color: Colors.grey[100],
child: const Icon(Icons.restaurant, color: Colors.grey, size: 20),
),
);
},
);
}
// 内容文字(最多3行,超出省略)
Widget _buildContentText() {
return Text(
dynamicModel.contentText,
style: const TextStyle(fontSize: 14, height: 1.5),
maxLines: 3,
overflow: TextOverflow.ellipsis,
);
}
// 互动区(点赞/收藏/评论/分享)
Widget _buildInteractionArea() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInteractionItem(Icons.favorite_border, dynamicModel.likeCount.toString()),
_buildInteractionItem(Icons.star_border, dynamicModel.collectCount.toString()),
_buildInteractionItem(Icons.comment, dynamicModel.commentCount.toString()),
_buildInteractionItem(Icons.share, dynamicModel.shareCount.toString()),
],
);
}
// 单个互动项(图标+数字)
Widget _buildInteractionItem(IconData icon, String count) {
return Column(
children: [
Icon(icon, size: 20, color: Colors.grey),
const SizedBox(height: 4),
Text(count, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
);
}
// 商品购买区(商品图+信息+购买按钮)
Widget _buildProductArea() {
final product = dynamicModel.productInfo!;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
// 商品图片(占位,后续可替换为本地商品图)
Container(
width: 60,
height: 60,
color: Colors.grey[300],
child: const Center(child: Text('商品图')),
// 可选:后续替换为本地商品图的写法
// child: Image.asset(
// 'assets/images/food_note/product/product1.jpg',
// fit: BoxFit.cover,
// errorBuilder: (_, __, ___) => const Text('商品图'),
// ),
),
const SizedBox(width: 8),
// 商品信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
Text(
'¥${product.price.toStringAsFixed(1)}',
style: const TextStyle(fontSize: 16, color: Colors.red, fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
Text(
'¥${product.originalPrice.toStringAsFixed(1)}',
style: const TextStyle(fontSize: 12, color: Colors.grey, decoration: TextDecoration.lineThrough),
),
],
),
],
),
),
// 购买按钮
ElevatedButton(
onPressed: () {}, // 后续可加购买跳转逻辑
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: const Text('去购买', style: TextStyle(color: Colors.white, fontSize: 12)),
),
],
),
);
}
}
4.3 动态 - 最热页面
👇 动态 - 最热页面(food_note_hot_tab.dart):静态列表
实现 "最热" 页面的静态动态列表,使用本地资源和模拟数据,代码如下:
// lib/pages/food_note/tab/ # 顶部Tab子页面
// lib/pages/food_note/tab/food_note_hot_tab.dart # 最热Tab页面(本地图片)
import 'package:flutter/material.dart';
import 'package:flutter_harmonyos/pages/food_note/components/dynamic_card.dart';
import 'package:flutter_harmonyos/pages/food_note/models/dynamic_model.dart';
class FoodNoteHotTab extends StatefulWidget {
const FoodNoteHotTab({super.key});
@override
State<FoodNoteHotTab> createState() => _FoodNoteHotTabState();
}
class _FoodNoteHotTabState extends State<FoodNoteHotTab> {
// 模拟最热动态数据(本地图片)
final List<DynamicModel> _hotDynamicList = [
DynamicModel(
id: '1',
// 替换为本地头像路径(avatar1.jpg)
userAvatar: 'assets/images/food_note/avatar/avatar1.jpg',
userName: '吃货菌de日常',
publishTime: '05月20日',
// 替换为本地内容图路径(循环用food1/2/3)
contentImages: [
'assets/images/food_note/dynamic/food1.jpg',
'assets/images/food_note/dynamic/food2.jpg',
'assets/images/food_note/dynamic/food3.jpg',
],
contentText: '这个神奇的茶水,就是苦荞茶。苦荞茶当中含有丰富的膳食纤维以及多种氨基酸,一方面能够帮助促进肠道蠕动,增加身体脂肪的燃烧,热量消耗。"五谷之王"苦荞做的茶,具有减脂瘦身、...',
likeCount: 1783,
collectCount: 3042,
commentCount: 128,
shareCount: 56,
productInfo: ProductInfo(
name: '1罐装 苦荞茶四川大凉山...',
price: 5.1,
originalPrice: 30.1,
buyUrl: 'https://example.com/buy', // 商品链接可后续换本地/其他
),
),
DynamicModel(
id: '2',
// 替换为本地头像路径(avatar2.jpg)
userAvatar: 'assets/images/food_note/avatar/avatar2.jpg',
userName: '美食达人',
publishTime: '05月19日',
// 替换为本地内容图路径(food2.jpg)
contentImages: [
'assets/images/food_note/dynamic/food2.jpg',
],
contentText: '#解馋零食 追剧追剧就靠这些零食了!',
likeCount: 2567,
collectCount: 1890,
commentCount: 234,
shareCount: 89,
productInfo: null,
),
DynamicModel(
id: '3',
// 替换为本地头像路径(avatar3.jpg)
userAvatar: 'assets/images/food_note/avatar/avatar3.jpg',
userName: '下饭神器',
publishTime: '05月18日',
// 替换为本地内容图路径(food3.jpg)
contentImages: [
'assets/images/food_note/dynamic/food3.jpg',
'assets/images/food_note/dynamic/food1.jpg',
],
contentText: '#下饭神器 好吃到逆天,吃了都想再两碗饭',
likeCount: 3456,
collectCount: 2789,
commentCount: 345,
shareCount: 123,
productInfo: null,
),
];
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _hotDynamicList.length,
itemBuilder: (context, index) {
return DynamicCard(dynamicModel: _hotDynamicList[index]);
},
);
}
}
▶️ 实现效果:

4.4 动态 - 最新页面
👇 动态 - 最新页面(food_note_new_tab.dart):静态列表
模拟数据更新为 "最新" 时间排序,代码如下:
// lib/pages/food_note/tab/ # 顶部Tab子页面
// lib/pages/food_note/tab/food_note_new_tab.dart # 最新Tab页面(本地图片)
import 'package:flutter/material.dart';
import 'package:flutter_harmonyos/pages/food_note/components/dynamic_card.dart';
import 'package:flutter_harmonyos/pages/food_note/models/dynamic_model.dart';
class FoodNoteNewTab extends StatefulWidget {
const FoodNoteNewTab({super.key});
@override
State<FoodNoteNewTab> createState() => _FoodNoteNewTabState();
}
class _FoodNoteNewTabState extends State<FoodNoteNewTab> {
// 模拟「最新」动态数据(本地图片版)
final List<DynamicModel> _newDynamicList = [
DynamicModel(
id: '4',
// 替换为本地头像路径(avatar4.jpg)
userAvatar: 'assets/images/food_note/avatar/avatar4.jpg',
userName: '新用户123',
publishTime: '05月21日',
// 替换为本地内容图路径(food4.jpg,或复用food1.jpg)
contentImages: [
'assets/images/food_note/dynamic/food4.jpg',
],
contentText: '今天发现了超好吃的甜品,推荐给大家!',
likeCount: 123,
collectCount: 45,
commentCount: 12,
shareCount: 5,
productInfo: null,
),
DynamicModel(
id: '5',
// 替换为本地头像路径(avatar5.jpg)
userAvatar: 'assets/images/food_note/avatar/avatar5.jpg',
userName: '美食爱好者',
publishTime: '05月21日',
// 替换为本地内容图路径(循环复用food1-4)
contentImages: [
'assets/images/food_note/dynamic/food1.jpg',
'assets/images/food_note/dynamic/food2.jpg',
'assets/images/food_note/dynamic/food3.jpg',
'assets/images/food_note/dynamic/food4.jpg',
],
contentText: '自制火锅教程,简单又好吃,新手也能学会!',
likeCount: 456,
collectCount: 234,
commentCount: 67,
shareCount: 23,
productInfo: ProductInfo(
name: '火锅底料套餐',
price: 19.9,
originalPrice: 39.9,
buyUrl: 'https://example.com/buy2', // 商品链接可后续替换
),
),
];
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _newDynamicList.length,
itemBuilder: (context, index) {
return DynamicCard(dynamicModel: _newDynamicList[index]);
},
);
}
}
▶️ 实现效果:

4.5 数据模型
数据模型(dynamic_model.dart)
// lib/pages/food_note/models/ # 动态数据模型
// dynamic_model.dart # 动态列表/商品数据结构
import 'package:flutter/material.dart';
// 动态数据模型
class DynamicModel {
final String id;
final String userAvatar; // 用户头像URL
final String userName; // 用户名
final String publishTime;// 发布时间
final List<String> contentImages; // 内容图片URL列表
final String contentText;// 内容文字
final int likeCount; // 点赞数
final int collectCount; // 收藏数
final int commentCount; // 评论数
final int shareCount; // 分享数
final ProductInfo? productInfo; // 商品信息(可选)
DynamicModel({
required this.id,
required this.userAvatar,
required this.userName,
required this.publishTime,
required this.contentImages,
required this.contentText,
required this.likeCount,
required this.collectCount,
required this.commentCount,
required this.shareCount,
this.productInfo,
});
}
// 商品信息模型
class ProductInfo {
final String name; // 商品名称
final double price; // 现价
final double originalPrice; // 原价
final String buyUrl; // 购买链接
ProductInfo({
required this.name,
required this.price,
required this.originalPrice,
required this.buyUrl,
});
}
5 运行验证与效果展示
5.1 鸿蒙设备运行
⚡️ 资源生效:确保pubspec.yaml配置正确后,执行flutter pub get刷新资源;
📱 启动设备:打开 DevEco Studio,启动鸿蒙模拟器(如 Mate 70 Pro);
💻 运行项目:VS Code 终端执行命令或DevEco Studio启动模拟器后点击右上角运行按钮:
🎯 功能验证:
✅ 1. 点击底部 "动态" 选项卡,确认 TabBar"最热 / 最新" 切换流畅;
✅ 2. 检查动态卡片:头像、图片加载正常,文字无溢出。
5.2 静态页面常见问题排查
⚠️ 问题现象:图片加载失败
🧰 排查方向:
📌 1. 检查pubspec.yaml中assets路径是否与实际目录一致(如assets/images/food_note/);
📌 2. 检查Image.asset路径是否正确(如少写assets/前缀);
📌 3. 执行flutter clean清除缓存后重新运行。
⚠️ 问题现象:Tab 切换无响应
🧰 排查方向:
📌 1. 确认TabController的length与TabBar、TabBarView的数量一致(均为 2);
📌 2. 检查with SingleTickerProviderStateMixin是否添加;
📌 3. 确保TabBar和TabBarView绑定同一_tabController。
6 代码提交
-
提交粒度:按功能模块拆分提交(如feat: 底部选项卡实现、feat: 首页轮播+搜索栏开发、fix: 页面状态保留优化);
-
Commit Message 规范:
⚡ feat: 新增功能(如底部选项卡、首页+美食+动态组件)
🔧 fix: 修复bug(如页面重复加载、布局错乱)
📊 docs: 文档更新(如 README、注释)
💻 refactor: 代码重构(如目录结构优化、组件复用,没有新增功能或修复Bug)
- 提交步骤
所需全部命令:
# 1. 查看修改
git status
# 2. 添加修改文件
git add .
# 3. 提交(规范Commit Message)
git commit -m "feat: 完成底部选项卡+动态功能实现"
# 4. 推送到AtomGit远程仓库
git push origin main




完成后可打开 AtomGit 仓库页面(在 AtomGit 创建的个人公开仓库),刷新页面:
确认仓库的"提交记录"中出现刚写的commit message;
刷新后的页面也能看到最新的提交记录和更新时间。

- 仓库要求:确保工程包含
pubspec.yaml、lib/源码、assets/资源、调试日志,可直接拉取复现运行效果。
7 总结与拓展
7.1 总结
🧾 本次 DAY8~DAY13 阶段已完成:
✅ 底部选项卡 "动态" 模块配置,实现与 "最热 / 最新" 子页面的联动;
✅ 2 个静态子页面 + 1 个动态卡片组件的 UI 搭建,适配本地资源加载;
✅ 开源鸿蒙设备静态展示验证,无布局错乱、资源缺失问题。
7.2 拓展方向
ℹ️ 说明:当前项目已完成首页、美食、动态模块核心功能与其他选项卡基础框架,后续将继续完善其他页面功能、补充完整模块与异常处理,持续优化项目体验。
🎯 后续将继续完善项目:
📌 完善其他选项卡页面功能(推荐、我的页面);
📌 补充完整功能模块与异常处理,优化整体体验。
📌 互动功能(关注 / 点赞):
🧾 1. 给 "关注" 按钮添加点击事件,调用关注接口;
🧾 2. 实现点赞 / 评论 / 收藏的状态切换;
🧾 3. 新增 "已关注""已点赞" 样式(如橙色图标)。
📌 动态详情页:
🧾 1. 点击动态卡片跳转详情页(FoodNoteDetailPage);
🧾 2. 展示完整动态内容、评论列表;
🧾 3. 支持分享、举报等功能。
📌 性能优化:
🧾 1. 实现列表加载(infinite_scroll_pagination库);
🧾 2. 优化本地资源压缩。
8 参考资料
🔗 参考前文:
https://blog.csdn.net/m0_74451345/article/details/156915775?spm=1001.2014.3001.5501
https://blog.csdn.net/m0_74451345/article/details/157024056?spm=1001.2014.3001.5501
https://blog.csdn.net/m0_74451345/article/details/157032531?spm=1001.2014.3001.5501
https://blog.csdn.net/m0_74451345/article/details/157464927?spm=1001.2014.3001.5501
https://blog.csdn.net/m0_74451345/article/details/157505773?spm=1001.2014.3001.5501
🔗 参考三方库:
OpenHarmony兼容的三方库:https://gitcode.com/openharmony-tpc/flutter_packages
🔗文章中AtomGit开源项目个人仓库链接:
https://atomgit.com/zhangxupeng2025/flutter_HmProject
ℹ️ 博客说明:本文为训练营实战记录,代码可在我的AtomGit个人公开仓库克隆到本地配置后直接运行(部分资源需要本地配置,比如需替换本地图片资源),后续将持续更新任务中心交互、其他底部选项卡模块等功能,文中也有许多待优化点,欢迎大家关注交流~
最后,
欢迎加入开源鸿蒙跨平台社区: