【开源鸿蒙跨平台开发先锋训练营】DAY8~DAY13 底部选项卡&动态功能实现

【开源鸿蒙跨平台开发先锋训练营】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 代码提交

  1. 提交粒度:按功能模块拆分提交(如feat: 底部选项卡实现、feat: 首页轮播+搜索栏开发、fix: 页面状态保留优化);

  2. Commit Message 规范:

⚡ feat: 新增功能(如底部选项卡、首页+美食+动态组件)

🔧 fix: 修复bug(如页面重复加载、布局错乱)

📊 docs: 文档更新(如 README、注释)

💻 refactor: 代码重构(如目录结构优化、组件复用,没有新增功能或修复Bug)

  1. 提交步骤

所需全部命令:

复制代码
# 1. 查看修改
git status
# 2. 添加修改文件
git add .
# 3. 提交(规范Commit Message)
git commit -m "feat: 完成底部选项卡+动态功能实现"
# 4. 推送到AtomGit远程仓库
git push origin main

完成后可打开 AtomGit 仓库页面(在 AtomGit 创建的个人公开仓库),刷新页面:

确认仓库的"提交记录"中出现刚写的commit message;

刷新后的页面也能看到最新的提交记录和更新时间。

  1. 仓库要求:确保工程包含pubspec.yamllib/源码、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个人公开仓库克隆到本地配置后直接运行(部分资源需要本地配置,比如需替换本地图片资源),后续将持续更新任务中心交互、其他底部选项卡模块等功能,文中也有许多待优化点,欢迎大家关注交流~

最后,

欢迎加入开源鸿蒙跨平台社区:

https://openharmonycrossplatform.csdn.net

相关推荐
_waylau2 小时前
首本鸿蒙架构师培养手册《鸿蒙架构师修炼之道》简介
华为·harmonyos·鸿蒙·鸿蒙系统·仓颉·cangjie
向哆哆2 小时前
Flutter × OpenHarmony 实战 | 打造画栈平台的顶部横幅组件
flutter·开源·鸿蒙·openharmony·开源鸿蒙
2501_944525543 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
android·开发语言·前端·javascript·flutter·ecmascript
雨季6663 小时前
Flutter 三端应用实战:OpenHarmony 简易“动态主题切换卡片”交互模式
flutter·ui·交互·dart
熊猫钓鱼>_>3 小时前
【开源鸿蒙跨平台开发先锋训练营】Day 19: 开源鸿蒙React Native动效体系构建与混合开发复盘
react native·华为·开源·harmonyos·鸿蒙·openharmony
向哆哆3 小时前
构建健康档案管理快速入口:Flutter × OpenHarmony 跨端开发实战
flutter·开源·鸿蒙·openharmony·开源鸿蒙
2601_949593653 小时前
基础入门 React Native 鸿蒙跨平台开发:BackHandler 返回键控制
react native·react.js·harmonyos
mocoding3 小时前
使用Flutter强大的图标库fl_chart优化鸿蒙版天气预报温度、降水量、湿度展示
flutter·华为·harmonyos
向哆哆4 小时前
构建智能健康档案管理与预约挂号系统:Flutter × OpenHarmony 跨端开发实践
flutter·开源·鸿蒙·openharmony·开源鸿蒙