【开源鸿蒙跨平台开发先锋训练营】DAY7 第一阶段知识要点复盘
本文基于本人在开源鸿蒙跨平台开发先锋训练营第一阶段(DAY1~DAY6)的学习成果复盘,主要聚焦Flutter+开源鸿蒙项目的第一阶段核心任务完成总结。
【开源鸿蒙跨平台开发先锋训练营】DAY7 第一阶段知识要点复盘
目录
【开源鸿蒙跨平台开发先锋训练营】DAY7 第一阶段知识要点复盘
摘 要
本文基于开源鸿蒙跨平台开发先锋训练营第一阶段(DAY1~DAY6)的学习成果,主要聚焦Flutter+开源鸿蒙项目的第一阶段核心任务。DAY7的核心目标是梳理第一阶段掌握的知识要点+创建问题解决以及对博文内容按照规则及导师意见进行优化调整,按照完成开源鸿蒙跨平台开发环境搭建、多终端工程创建运行,集成网络请求能力,实现数据清单列表,列表清单上拉加载、下拉刷新及数据加载提示能力的功能验证几项步骤。通过其中集成图片缓存组件、规范代码结构、配置鸿蒙应用签名,实现功能验证流程。本文将尽可能详细阐述总结各环节的技术原理与实操步骤,为跨平台项目的工程化落地提供参考。
1 概述
1.1 第一阶段任务背景
开源鸿蒙跨平台开发先锋训练营第一阶段通过7天递进式任务,构建从环境搭建到功能实现的技术体系。本人在开源鸿蒙跨平台开发先锋训练营第一阶段(DAY1~DAY6)中主要围绕“Flutter + 开源鸿蒙跨平台本地美食清单”应用展开,完成从开发环境搭建、核心功能实现到交互体验优化的功能实现,覆盖环境配置、Dio网络请求集成、美食清单列表UI功能实现、下拉刷新与上拉加载分页机制交互、加载状态数据提示等跨平台开发核心环节。
1.2 第一阶段任务定位与目标
DAY7是训练营第一阶段复盘,重点解决前面开发的性能短板与工程化落地问题:通过性能优化提升应用体验,验证跨平台方案的可行性与稳定性,为后续项目打好基础。
核心目标包含:
1. 梳理第一阶段知识要点,形成结构化知识框架;
2. 按技术规范,优化CSDN博文的结构、细节与可读性;
3. 总结技术收获与待优化点,明确后续学习方向。
2 环境准备与功能实现
2.1 基础环境搭建(DAY1~DAY2)
2.1.1 开发环境配置
| 开发工具/环境 | 版本规格 | 用途说明 |
| 操作系统 | Windows 11 64 位 | 开发主机运行环境 |
| VS Code | 1.108.1(user setup) | Flutter 业务代码编写与依赖管理 |
| DevEco Studio | 6.0.0 Release | OpenHarmony 应用配置与部署 |
| OpenHarmony SDK | API Version 20(6.0.0.47) | 鸿蒙应用开发核心依赖 |
| Flutter | 3.27.4(鸿蒙适配版) | 跨平台 UI 框架 |
2.1.2 项目初始化
完成开发环境搭建后,我们就要开始创建Flutter 跨平台项目了。
1. 首先,我们可以在电脑上选择一个空间充足的磁盘位置,创建专门存放Flutter For OpenHarmony项目的文件夹,今后创建开发Flutter跨平台项目可以专门存放到这个文件夹中。比如我之前创建的“Flutter_HmProject”的文件夹就专门用来存放Flutter 项目。需要创建Flutter 跨平台项目与鸿蒙平台适配(flutter create --platforms ohos);
flutter create --platforms ohos <项目名称>
2. 工程目录规范(API 层、UI 层、模型层、配置层的目录划分逻辑)。
(详细)项目根目录/
├─ ohos/ # OpenHarmony 原生工程目录(鸿蒙侧配置/原生代码,保持鸿蒙标准结构)
│ ├─ entry/ # 鸿蒙应用入口模块(当前的ohos/entry)
│ │ ├─ src/main/ets/ # 鸿蒙ETS代码(原生页面/能力)
│ │ │ ├─ module.json5 # 鸿蒙权限/配置文件(当前的配置)
│ │ └─ ...(鸿蒙其他原生配置)
│ │─ ...(鸿蒙其他模块)
│ │
├─ lib/ # Flutter 业务代码目录(核心分层,重点优化)
│ ├─ core/ # 核心基础封装(复用性强的底层能力)
│ │ ├─ http/ # 网络请求封装(当前的core/http)
│ │ ├─ http_client.dart # 网络请求工具类
│ │ └─ api_config.dart # 网络配置(baseUrl、超时等)
│ ├─ api/ # 业务接口层(当前的api)
│ │ └─ food_api.dart # 美食相关接口(getFoodList)
│ │
│ ├─ models/ # 数据模型层(当前的models)
│ │ ├─ food_model.dart # 美食实体类
│ │ └─ food_model.g.dart # (若用json_serializable,自动生成的模型)
│ │
│ ├─ pages/ # 页面层(按业务模块划分,你当前的pages)
│ │ └─ food/ # 美食业务模块
│ │ └─ food_list_page.dart # 美食列表页面
│ └─ main.dart # Flutter入口文件
├─ pubspec.yaml # Flutter依赖配置
2.1.3 依赖配置
| 依赖名称 | 版本 | 功能说明 |
| pull_to_refresh |
^2.0.0 |
下拉刷新组件 |
| infinite_scroll_pagination |
4.0.0 |
上拉分页加载组件 |
| dio |
5.0.0 |
增强型网络请求库 |
| json_annotation |
^4.9.0 |
JSON 序列化注解工具 |
2.1.4 环境准备常见问题与排查
本人在前期环境搭建过程中,也遇到了一些问题:
1. infinite_scroll_pagination 版本兼容问题
现象:使用 3.2.0 版本时,构建代码出现两处报错:
可能原因:3.2.0 版本的库与我目前使用的鸿蒙 Flutter(高版本 Flutter)不兼容。
解决:升级到 4.0.0 版本后,兼容问题得到修复。
2. Node.js 文件修改后生效延迟
现象:修改 Node.js 相关配置/代码后,终端执行 node server.js 未立即生效,重启进程后才出现理想效果。
原因:Node.js进程默认不监听文件变化(无热重载机制),或缓存残留导致未加载新代码。
3. Flutter 依赖安装异常
现象:构建时提示 .dart_tool/package_config.json 文件缺失,报错:“Did you run this command from the same directory as your pubspec.yaml file?”。
原因:未在项目根目录(pubspec.yaml 所在目录)执行 flutter pub get,导致依赖未正确安装,配置文件未生成。
解决:需要在VS Code终端中切换到项目根目录执行 flutter pub get 生成配置文件,再执行 flutter clean 清理缓存后重建项目。
2.2 网络请求与数据列表(DAY3)
2.2.1 Dio 网络请求封装
本模块主要基于 Dio 库封装美食列表的网络请求,核心实现文件为我的项目中的 lib/api/food_api.dart文件,关键逻辑如下:
1. 请求方法封装:
- 定义
FoodApi类的静态方法getFoodList,无需实例化即可调用; - 强制接收
page(页码)、pageSize(每页数据量)两个分页参数,保证请求参数的完整性。
2. 分页参数传递:
- 构造
queryParams字典封装分页参数,通过dio.get的queryParameters传递给后端接口,实现分页数据请求; - 接口地址从
ApiConfig配置文件读取(将接口地址与请求逻辑分离,通过配置文件统一管理接口地址,避免硬编码,提升代码可维护性。)。
3. 异常与日志处理:
- 通过
try-catch捕获请求异常,打印错误日志并向上抛异常,让页面层(food_list_page.dart)统一处理加载失败逻辑; - 增加请求 / 返回日志打印,便于调试时定位问题。
// food_api.dart
// 先导入依赖包
import 'package:dio/dio.dart';
import '../core/http/api_config.dart'; // 对应api_config的路径
// 定义FoodApi类
class FoodApi {
// 改为类的静态方法,并接收分页参数page/pageSize
static Future<dynamic> getFoodList({
required int page, // 页码
required int pageSize, // 每页数据量
}) async {
try {
Dio dio = Dio();
// 构造分页请求参数(传递给接口的query参数)
final queryParams = {
"page": page,
"pageSize": pageSize,
};
print("请求接口:${ApiConfig.food_list_url},参数:$queryParams"); // 打印请求地址
// 发起请求时携带分页参数,调用api_config中配置的接口地址
Response response = await dio.get(
ApiConfig.food_list_url,
queryParameters: queryParams, // 关键:把分页参数传给后端接口
);
print("接口返回:${response.data}"); // 打印返回数据
return response.data; // 提取接口返回的美食列表数据
} catch (e) {
print("接口请求失败:$e"); // 打印错误日志
throw e; // 向上抛异常,让页面层处理
}
}
}
2.2.2 数据模型与 JSON 解析
food_model.dart 是数据模型与JSON解析的核心文件,包含模型定义、单模型解析、列表模型解析、解析安全处理全部关键逻辑;
1. 数据模型定义(映射 JSON 字段)
- 定义 FoodModel 类,字段(id/name/desc/image/score)一一对应后端返回 JSON 的字段,实现 “JSON 字段 → Dart 模型属性” 的映射;
- 定义 FoodListModel 类封装列表数据,解决 “JSON 数组 → Dart 模型列表” 的解析需求。
2. JSON 解析实现(单模型 + 列表模型)
- 单个模型解析:
借助 json_annotation 注解(@JsonSerializable()),通过 fromJson 方法(_$FoodModelFromJson)实现 JSON 转 FoodModel 对象(解析逻辑由自动生成的 food_model.g.dart 实现,无需手动写字段映射);
同时提供 toJson 方法,支持模型对象转回 JSON(满足后续可能的 “提交数据” 场景)。
- 列表模型解析:
FoodListModel.fromJson 手动处理 JSON 数组解析:先判断 json['foodList'] 存在且为 List 类型,再通过 map 遍历数组,逐个将子 JSON 转 FoodModel,最终组装成 FoodListModel 列表模型,避免空值 / 类型错误导致崩溃。
3. 解析安全处理(隐性但重要的点)
- 字段用 int?/String?/double? 可空类型定义,避免后端返回 null 导致解析崩溃;
- 列表解析时增加 json['foodList'] != null && json['foodList'] is List 双重判断,保证解析逻辑的健壮性。
// food_model.dart
import 'package:json_annotation/json_annotation.dart';
part 'food_model.g.dart'; // 关联自动生成的解析代码文件
@JsonSerializable()
class FoodModel {
final int? id;
final String? name; // 对应接口的name
final String? desc; // 对应接口的desc
final String? image; // 对应接口的image
final double? score; // 美食评分(比如4.5、3.8)
FoodModel({
this.id,
this.name,
this.desc,
this.image,
this.score, // 构造函数添加score
});
// 自动生成的JSON转模型方法(由g.dart实现)
factory FoodModel.fromJson(Map<String, dynamic> json) => _$FoodModelFromJson(json);
// 自动生成的模型转JSON方法(由g.dart实现)
Map<String, dynamic> toJson() => _$FoodModelToJson(this);
}
// 美食列表模型
class FoodListModel {
final List<FoodModel> foodList;
FoodListModel({required this.foodList});
// 列表数据解析(简化格式),从JSON数组构建列表模型
factory FoodListModel.fromJson(Map<String, dynamic> json) {
List<FoodModel> list = []; // 声明list变量
if (json['foodList'] != null && json['foodList'] is List) { // 逻辑与用&&,判断foodList存在且是List类型
list = (json['foodList'] as List)
.map((e) => FoodModel.fromJson(e as Map<String, dynamic>))
.toList();
}
return FoodListModel(foodList: list);
}
}
自动生成解析代码 :food_model.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'food_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FoodModel _$FoodModelFromJson(Map<String, dynamic> json) => FoodModel(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
desc: json['desc'] as String?,
image: json['image'] as String?,
score: (json['score'] as num?)?.toDouble(),
);
Map<String, dynamic> _$FoodModelToJson(FoodModel instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'desc': instance.desc,
'image': instance.image,
'score': instance.score,
};
2.2.3 基础UI与列表功能
在 _buildFoodItem 方法中,直接使用了 Flutter 原生布局组件,并适配鸿蒙设备的布局显示:
- Card 组件:作为列表项的容器,设置了 margin 实现鸿蒙设备上的间距兼容性;
- Row 组件:横向布局图片与美食信息区域,适配鸿蒙设备的行布局逻辑;
- Column 组件:纵向布局美食名称、描述、评分组件,适配鸿蒙设备的列布局逻辑。
// 列表项UI构建
Widget _buildFoodItem(FoodModel food) {
// 新增打印,确认图片URL是否正确(复制这个URL到手机浏览器能打开)
final imageUrl = "http://192.168.0.108:3000${food.image ?? ""}";
print("图片请求URL:$imageUrl"); // 看Flutter控制台日志,确认URL正确
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
// 原生Image.network
Image.network(
imageUrl, // 用上面拼接好的URL
width: 80,
height: 80,
fit: BoxFit.cover, // 让图片填充容器
// 加载中显示转圈
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const CircularProgressIndicator();
},
// 加载失败打印错误+显示图标
errorBuilder: (context, error, stackTrace) {
print("图片加载失败原因:${error.toString()}");
return const Icon(Icons.fastfood);
},
),
const SizedBox(width: 12),
// 美食信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 美食名称
Text(
food.name ?? "未知美食",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
// 美食描述
Text(
food.desc ?? "暂无描述",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 4),
// 评分组件
_buildNativeRating(food.score ?? 0),
],
),
),
],
),
),
);
}
2.2.4 常见问题与排查
鸿蒙设备网络权限未配置
问题表现:在鸿蒙设备上运行应用时,接口请求无响应,控制台打印 “网络连接失败” 但无具体数据返回。
原因:未在鸿蒙工程的ohos/entry/src/main/module.json5 的 reqPermissions 节点中声明ohos.permission.INTERNET权限,导致应用无网络访问权限。
解决:在鸿蒙模块的ohos/entry/src/main/module.json5 的 reqPermissions 节点中添加权限配置:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
2.3 交互功能与提示优化(DAY4~DAY6)
2.3.1 分页交互机制
1. 下拉刷新功能实现(pull_to_refresh组件的RefreshController与onRefresh回调逻辑);
pull_to_refresh 组件通过 RefreshController 管理下拉状态(初始状态、刷新中、刷新完成),通过 onRefresh 回调关联 “下拉动作” 与 “数据请求逻辑”,实现 “用户下拉 → 触发请求 → 更新列表 → 结束刷新” 的流程。
交互逻辑:
用户在页面下拉列表区域,触发 SmartRefresher 的下拉事件;
组件调用 onRefresh 绑定的 _getFoodListData(isRefresh: true) 方法;
方法内先重置分页状态(清空历史数据、恢复初始页码),再请求第 1 页数据;
数据请求成功后,替换原有列表数据,并更新分页控制器的初始数据;
最后调用 _refreshController.refreshCompleted(),结束下拉刷新动画并更新提示 UI。
// 获取美食数据(新增isRefresh参数,区分刷新/加载)
Future<void> _getFoodListData({required bool isRefresh}) async { // _getFoodListData 方法
// 加载锁:防止重复请求
if (_isLoading) return;
_isLoading = true;
try {
if(isRefresh){ // 下拉刷新时:重置页码、数据、状态
_pagingController.refresh(); // 重置分页控制器,清空历史分页状态
await Future.delayed(const Duration(seconds: 2)); // 下拉刷新→加载中 停留2秒
} else {
await Future.delayed(const Duration(seconds: 2));// 上拉加载时加延迟(延长“加载中...”显示时间)
}
// 调用API层分页请求,下拉刷新永远请求第1页
final data = await FoodApi.getFoodList(
page: 1, // 下拉刷新永远请求第1页
pageSize: _pageSize,
);
if (mounted) {
if (data is Map<String, dynamic>) { // 先判断data是否是List类型
// 解析后端返回的Map格式数据(适配后端结构)
final foodListModel = FoodListModel.fromJson(data);
setState(() {
if (isRefresh) {
_foodList = foodListModel.foodList; // 下拉刷新:替换原有数据
} else {
_foodList.addAll(foodListModel.foodList); // 上拉加载:追加新数据
}
});
// 新增:下拉刷新成功后,给分页控制器赋值第一页数据
_pagingController.value = PagingState(// 更新分页控制器:设置第一页数据与下一页页码
nextPageKey: foodListModel.foodList.length >= _pageSize ? 2 : null,
itemList: foodListModel.foodList,
);
} else {
throw Exception("接口返回数据格式错误,不是对象格式");
}
}
} catch (e) {
if (mounted) {// 加载失败:显示错误提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("加载失败:${e.toString()}")),
);
}
} finally {
_isLoading = false;
if (isRefresh) {// 下拉刷新成功提示
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
_refreshController.refreshCompleted();// 结束下拉刷新动画(必须调用,否则刷新状态不会重置)
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("美食列表加载成功")),
);
}
});
}
}
}

2. 上拉加载分页功能实现(infinite_scroll_pagination的PagingController与addPageRequestListener监听)。
infinite_scroll_pagination 组件通过 PagingController 管理分页状态(当前页码、数据列表、是否有更多数据),通过 addPageRequestListener 监听 “列表滑到底部” 的事件,自动触发对应页码的数据请求;再通过 PagedListView 根据控制器状态渲染列表、加载中 / 无更多数据等提示。
交互逻辑:
用户滑动列表到最底部,触发 PagingController 的 addPageRequestListener 监听;
监听传入当前 “页码 key”,调用 _fetchPage(pageKey) 方法;
方法内请求对应页码的数据,解析后判断是否为最后一页(返回数据量 < 每页大小);
若为最后一页:调用 appendLastPage,分页控制器标记 “无更多数据”,PagedListView 渲染 “已加载全部数据” 提示;
若不是最后一页:调用 appendPage,追加新数据并传入 “下一页 key”,PagedListView 自动渲染新数据,等待用户再次滑到底部触发下一页请求。
// 上拉加载更多专用方法(pageKey为当前要加载的页码)
// 新增:infinite_scroll_pagination 专用分页请求方法
Future<void> _fetchPage(int pageKey) async {
if (_isLoading) return;
_isLoading = true;
try {
await Future.delayed(const Duration(seconds: 2));// 延长加载提示显示
final data = await FoodApi.getFoodList( // 调用API层,请求当前页码数据
page: pageKey,
pageSize: _pageSize,
);
if (mounted && data is Map<String, dynamic>) {
final foodListModel = FoodListModel.fromJson(data);
final newItems = foodListModel.foodList;
// 判断是否为最后一页(返回数据量 < 每页大小 → 无更多数据)
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
// 无更多数据:通知分页控制器停止上拉加载
_pagingController.appendLastPage(newItems);
} else {
// 有更多数据,自动加载下一页,页码+1,无限上拉核心逻辑
final nextPageKey = pageKey + 1;
_pagingController.appendPage(newItems, nextPageKey);
}
}
} catch (e) {
// 加载失败:通知分页控制器显示失败提示
_pagingController.error = e;
if(mounted){
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("加载失败:${e.toString()}")),
);
}
} finally {
_isLoading = false;
}
}

2.3.2 加载状态提示
1. 全场景状态提示
加载状态提示通过 pull_to_refresh 和 infinite_scroll_pagination 的状态构建器,结合请求逻辑中的提示,实现全场景状态的 UI 反馈,包括:初始化加载、刷新中、加载中、无更多数据、加载失败的 UI 实现等。
(1)刷新中提示
// 自定义下拉刷新头部(刷新中状态)
header: CustomHeader(
builder: (context, mode) {
String headerText = "";
Color textColor = Colors.black87; // 初始文字颜色
// 根据刷新状态切换文案和颜色
if (mode == RefreshStatus.refreshing) {
headerText = "刷新中"; // 刷新中显示“刷新中”
textColor = Colors.blueAccent; // 加载中用橙色,更醒目
}
// 其他状态处理...
return Container(
height: 60,
alignment: Alignment.center,
child: Text(headerText, // 给Text添加style,应用textColor和字体样式
style: TextStyle(
color: textColor, // 应用定义的文字颜色
fontSize: 16, // 加大字号,更醒目
fontWeight: FontWeight.w400, // 加粗字体
),
), // 显示对应的提示文本
);
},
),
原理:pull_to_refresh 组件的 CustomHeader 会根据下拉刷新的状态(mode)动态回调,通过判断 mode == RefreshStatus.refreshing 识别 “刷新中” 状态,从而更新 UI 文案与样式。
逻辑:用户下拉列表触发刷新 → 组件自动切换到 RefreshStatus.refreshing 状态 → 回调 builder 方法 → 渲染 “刷新中” 提示文案,让用户感知刷新进度。
(2)加载中提示
newPageProgressIndicatorBuilder: (_) => Container(
height: 60,
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(strokeWidth: 2),
SizedBox(width: 8),
Text("加载中..."),
],
),
),
原理:infinite_scroll_pagination 的 PagedListView 会监听 PagingController 的 “请求中” 状态,自动触发 newPageProgressIndicatorBuilder 构建加载中 UI。
逻辑:用户滑动列表到底部 → PagingController 触发分页请求 → 列表自动渲染 “加载中...” 的转圈 + 文字提示,避免用户重复滑动,同时明确加载状态。
(3)无更多数据提示
noMoreItemsIndicatorBuilder: (_) => Container( // 无更多数据提示
height: 60,
alignment: Alignment.center,
child: const Text("已加载全部数据"),
),
原理:当分页请求返回的数据量小于每页大小(isLastPage = newItems.length < _pageSize)时,调用 _pagingController.appendLastPage(newItems),PagingController 会标记 “无更多数据”,进而触发 noMoreItemsIndicatorBuilder。
逻辑:上拉请求返回数据不足一页 → 判定为 “无更多数据” → 分页控制器通知列表 → 渲染 “已加载全部数据” 提示,告知用户无需继续滑动。
(4)加载失败提示
// 1. 初始化加载失败提示
firstPageErrorIndicatorBuilder: (_) => Container(
height: 60,
alignment: Alignment.center,
child: const Text("加载失败,下拉重试"),
),
// 2. 上拉加载失败提示
newPageErrorIndicatorBuilder: (_) => Container(
height: 60,
alignment: Alignment.center,
child: const Text("加载失败,上拉重试"),
),
// 3. SnackBar操作提示(请求失败时)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("加载失败:${e.toString()}")),
),
原理:
列表层面的失败提示:PagingController 捕获请求异常后标记 “错误状态”,触发对应的失败构建器;
SnackBar 提示:通过 ScaffoldMessenger 全局触发轻量级提示,是 Flutter 原生的跨平台反馈组件。
逻辑:
请求过程中捕获异常 → 同时触发两种反馈:
(数据)列表层面:渲染 “加载失败,下拉/上拉重试” 的静态提示,引导用户操作;
全局提示:弹出 SnackBar 显示具体错误信息,让用户快速感知失败原因。
2. 原生组件扩展
这部分通过 Flutter 原生组件实现扩展,包括:自定义星级评分_buildNativeRating、SnackBar 操作提示的跨平台适配,天然适配鸿蒙等跨平台场景:
(1)自定义星级评分(_buildNativeRating)
// 原生星星评分组件(无需第三方包)
Widget _buildNativeRating(double score) {
const int maxStars = 5; // 满分5星
final int fullStars = score.floor(); // 全星数量(比如4.5→4)
final bool hasHalfStar = score - fullStars >= 0.5; // 是否有半星
final int emptyStars = maxStars - fullStars - (hasHalfStar ? 1 : 0); // 空星数量
return Row(
children: [
// 全星
...List.generate(fullStars, (index) => const Icon(
Icons.star,size: 18,color: Colors.amber,
)),
// 半星(如果有)
if (hasHalfStar)
const Icon(
Icons.star_half,size: 18,color: Colors.amber,
),
// 空星
...List.generate(emptyStars, (index) => const Icon(
Icons.star_border,size: 18,color: Colors.amber,
)),
],
);
}
原理:利用 Flutter 原生 Row + Icon 组件,通过 Dart 的列表生成(List.generate)和条件判断,动态拼接 “全星 + 半星 + 空星” 的 UI 结构,无需依赖第三方包。
逻辑:传入美食评分(如 4.5)→ 计算全星(4)、半星(是)、空星(0)的数量 → 动态生成对应 Icon 组成 Row → 渲染出符合评分的星级 UI,同时因使用 Flutter 原生组件,天然适配鸿蒙设备的布局与样式。
(2)SnackBar 操作提示
// 请求失败时的SnackBar提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("加载失败:${e.toString()}")),
);
// 刷新成功时的SnackBar提示
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("美食列表加载成功")),
);
原理:SnackBar 是 Flutter 框架中的一个内置 UI 组件,Flutter 框架会自动处理其在鸿蒙等平台的渲染逻辑,保证跨平台兼容性。
逻辑:请求成功/失败时 → 调用 ScaffoldMessenger.of(context).showSnackBar → 在页面底部弹出轻量级提示框 → 告知用户操作结果,同时适配鸿蒙设备的交互规范(如提示框的显示/消失动画、布局位置)。
2.3.3 常见问题与排查
1. 下拉刷新后动画一直转圈,无数据返回
问题表现:下拉触发刷新后,“刷新中” 动画持续转圈,页面无新数据加载,也不显示 “刷新完成” 提示。
核心原因:
未调用 _refreshController.refreshCompleted():请求完成后未通知下拉刷新控制器 “结束状态”,导致动画一直保持 “刷新中”;
请求异常时未重置状态:请求失败后未处理加载锁 / 控制器状态,导致控制器一直处于 “加载中”;
分页控制器未正确重置:下拉刷新时未调用 _pagingController.refresh(),历史分页状态残留导致新数据无法渲染。
解决方法:
无论请求成功/失败,在 finally 代码块中调用 _refreshController.refreshCompleted(),强制结束刷新动画;
下拉刷新时必须执行 _pagingController.refresh(),清空分页控制器的历史状态;
请求异常时(catch 块)手动重置 _isLoading = false,避免加载锁一直生效。
2. 上拉加载一直无数据(滑到底部无反应/加载失败)
问题表现:滑动列表到底部后,不显示 “加载中” 提示,也无新数据加载;或显示 “加载中” 后一直无结果。
核心原因:
页码参数传递错误:_fetchPage 中调用 FoodApi.getFoodList 时,未将 pageKey 作为 page 参数传递(比如写死为固定页码);
分页控制器的 nextPageKey 未正确设置:请求成功后,未根据 “当前页数据量” 判断是否有下一页,导致 nextPageKey 为空 / 未递增;
后端返回数据格式不匹配:接口返回数据不是 Map<String, dynamic> 格式,导致 FoodListModel.fromJson 解析失败,新数据无法追加。
解决方法:
确保 FoodApi.getFoodList(page: pageKey) 中 page 参数直接使用 pageKey(随上拉页码动态变化);
请求成功后,通过 newItems.length >= _pageSize 判断是否有下一页,正确设置 nextPageKey = pageKey + 1;
增加数据格式校验(如 if (data is Map<String, dynamic>)),避免解析异常导致数据丢失。
3 代码提交至 AtomGit 公开仓库
1. 在 AtomGit 创建个人公开仓库
项目功能初步完善后,我们可以将自己的项目上传到AtomGit 个人公开仓库中以便参考学习,首先需要在AtomGit官网有自己的帐号,之后创建好关于Flutter+开源鸿蒙个人项目的公开仓库,后续开发在该仓库中提交。
2. 准备本地工程与 Git 环境
首先,我们要确保工程根目录包含全部文件(lib源码、ohos鸿蒙工程配置、build编译文件等),之后开始初始化 Git 仓库(若未初始化),需要打开终端,进入工程根目录,比如我在前面博客中创建的D:\Flutter_HmProject\flutter_harmonyos项目,
cd D:\Flutter_HmProject\flutter_harmonyos # 先进入到工程根目录中
git init
配置 Git 用户信息/Git 全局设置(首次使用需配置):
git config --global user.name "你的AtomGit用户名"
git config --global user.email "你的AtomGit绑定邮箱"
输入以下命令,可以查看当前的 Git 配置信息,确认用户名和邮箱是否已经正确设置:
git config --list
3. 本地工程关联 AtomGit 仓库
将本地工程与 AtomGit 远程仓库关联(仓库地址是你的 AtomGit 仓库地址):
git remote add origin https://atomgit.com/你的用户名/仓库名称.git
4. 代码提交至 AtomGit 仓库
代码提交至 AtomGit 仓库前先要将本地工程文件添加到 Git 暂存区:
git add .
提交代码(填写清晰的 commit 信息):
git commit -m "Initial commit: Flutter+鸿蒙跨平台工程初始化"
执行推送命令(首次推送关联分支),将代码推送至 AtomGit 远程仓库:
git push -f origin main
首次将代码推送至 AtomGit 远程仓库,会提示身份验证,需要输入你的 AtomGit 账号 + 密码,或用访问令牌替代密码。
5. 验证推送结果
最后,进行验证推送结果,需要刷新你的 AtomGit 仓库页面,确认工程配置文件、源码、资源、调试日志等文件已全部显示,完成代码提交任务。
4 第一阶段复盘总结与后续学习方向
4.1 技术收获与能力沉淀
在开源鸿蒙跨平台开发先锋训练营第一阶段(DAY1~DAY6)中,我主要围绕“Flutter + 开源鸿蒙本地美食清单”应用进行开发,我完成了从“环境搭建→功能实现→交互优化”的技术步骤,具体收获如下:
1. 跨平台开发环境搭建能力
在前期学习准备过程中,我能够通过部署环境掌握到 Windows 11 系统下 Flutter + 开源鸿蒙的开发环境配置逻辑:包括 Flutter SDK、DevEco Studio、HarmonyOS SDK 的安装与版本适配,实现了 VS Code(Flutter 编码)与 DevEco Studio(鸿蒙打包)的工具协同,同时掌握了运行工具对开源鸿蒙模拟器的连接、调试流程,能够独立解决设备识别、调试权限等环境类基础问题。
2. 核心功能开发与技术落地能力
(1)网络请求层:完成了 Dio 网络请求库的封装,实现了 “美食列表分页接口” 的参数传递(page/pageSize)、异常捕获与错误透传,能适配后端 Map 格式的返回数据;
(2)数据处理层:设计了FoodModel/FoodListModel的数据模型,掌握了fromJson方法对 JSON 数据的解析逻辑,实现了字段空安全、默认值处理等可扩展设计;
(3)UI 与列表层:完成了 “本地美食清单” 的基础 UI 渲染(Card/Row等组件的跨平台布局),能基于业务需求封装自定义组件(如原生星级评分_buildNativeRating)。
3. 交互体验优化与用户反馈能力
在第一阶段项目完善过程中,也实现了全场景的交互功能与状态提示:通过pull_to_refresh组件完成下拉刷新逻辑,借助infinite_scroll_pagination实现上拉加载分页机制,同时覆盖了“初始化加载、刷新中、加载中、无更多数据、加载失败”的场景状态UI提示,掌握了SnackBar等原生组件的跨平台适配方法,能够通过细节设计来提升应用的用户体验。
4. 问题排查与工程化思维
在实现不同功能过程中,我也遇到不同问题,但最终都已合理解决掉,从而形成了“问题场景→分层排查→方法解决” 的技术思维:针对接口请求失败、列表卡顿、数据解析异常等问题,能通过 “网络连通性校验→权限配置检查→代码逻辑定位” 的步骤快速进行排查;同时建立了“API 层 - UI 层 - 模型层 - 配置层”的工程目录规范,以此来保证代码的可维护性与复用性。
4.2 待优化点与后续学习计划
结合第一阶段的开发实践,目前仍存在技术深度与内容输出的优化空间,后续大致学习计划如下:
1. 技术深度提升计划
(1)拓展鸿蒙原生能力:目前仅实现了基础跨平台功能,后续计划探索 Flutter 与开源鸿蒙原生能力的交互,如调用系统相册、申请位置权限等,提升应用的原生适配性;
(2)优化性能瓶颈:针对列表图片加载卡顿的问题,后续将完善图片缓存策略(如集成cached_network_image),同时优化分页数据量的动态控制逻辑。
2. 内容输出与知识沉淀计划
(1)系列博文一致性优化:持续统一技术术语(如规范使用一些专业术语的表述,方便理解);
(2)强化技术佐证材料:后续将补充项目应用运行效果图、代码修复前后的对比片段等内容,增强博文的真实性与说服力。
欢迎加入开源鸿蒙跨平台社区:
更多推荐


所有评论(0)