【开源鸿蒙跨平台开发先锋训练营】DAY7 第一阶段知识要点复盘

【开源鸿蒙跨平台开发先锋训练营】DAY7 第一阶段知识要点复盘

目录

[【开源鸿蒙跨平台开发先锋训练营】DAY7 第一阶段知识要点复盘](#【开源鸿蒙跨平台开发先锋训练营】DAY7 第一阶段知识要点复盘)

[摘 要](#摘 要)

[1 概述](#1 概述)

[1.1 第一阶段任务背景](#1.1 第一阶段任务背景)

[1.2 第一阶段任务定位与目标](#1.2 第一阶段任务定位与目标)

[2 环境准备与功能实现](#2 环境准备与功能实现)

[2.1 基础环境搭建(DAY1~DAY2)](#2.1 基础环境搭建(DAY1~DAY2))

[2.1.1 开发环境配置](#2.1.1 开发环境配置)

[2.1.2 项目初始化](#2.1.2 项目初始化)

[2.1.3 依赖配置](#2.1.3 依赖配置)

[2.1.4 环境准备常见问题与排查](#2.1.4 环境准备常见问题与排查)

[2.2 网络请求与数据列表(DAY3)](#2.2 网络请求与数据列表(DAY3))

[2.2.1 Dio 网络请求封装](#2.2.1 Dio 网络请求封装)

[2.2.2 数据模型与 JSON 解析](#2.2.2 数据模型与 JSON 解析)

[2.2.3 基础UI与列表功能](#2.2.3 基础UI与列表功能)

[2.2.4 常见问题与排查](#2.2.4 常见问题与排查)

[2.3 交互功能与提示优化(DAY4~DAY6)](#2.3 交互功能与提示优化(DAY4~DAY6))

[2.3.1 分页交互机制](#2.3.1 分页交互机制)

[2.3.2 加载状态提示](#2.3.2 加载状态提示)

[2.3.3 常见问题与排查](#2.3.3 常见问题与排查)

[3 代码提交至 AtomGit 公开仓库](#3 代码提交至 AtomGit 公开仓库)

[4 第一阶段复盘总结与后续学习方向](#4 第一阶段复盘总结与后续学习方向)

[4.1 技术收获与能力沉淀](#4.1 技术收获与能力沉淀)

[4.2 待优化点与后续学习计划](#4.2 待优化点与后续学习计划)


摘 要

本文基于开源鸿蒙跨平台开发先锋训练营第一阶段(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.getqueryParameters 传递给后端接口,实现分页数据请求;
  • 接口地址从 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 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("美食列表加载成功")),
            );
          }
        });
      }
  }
}
  1. 上拉加载分页功能实现(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 显示具体错误信息,让用户快速感知失败原因。

  1. 原生组件扩展

这部分通过 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,避免加载锁一直生效。

  1. 上拉加载一直无数据(滑到底部无反应/加载失败)

问题表现:滑动列表到底部后,不显示 "加载中" 提示,也无新数据加载;或显示 "加载中" 后一直无结果。

核心原因:

页码参数传递错误:_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)强化技术佐证材料:后续将补充项目应用运行效果图、代码修复前后的对比片段等内容,增强博文的真实性与说服力。

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

https://openharmonycrossplatform.csdn.net

相关推荐
夜雨声烦丿2 小时前
Flutter 框架跨平台鸿蒙开发 - 育儿知识大全应用开发教程
flutter·华为·harmonyos
kirk_wang2 小时前
Flutter艺术探索-Flutter发布应用:Android与iOS打包流程
flutter·移动开发·flutter教程·移动开发教程
程序员老刘·3 小时前
跨平台开发地图:2025跨平台技术简单总结 | 2026年1月
flutter·跨平台开发·客户端开发
寒季66610 小时前
Electron 实战:构建跨平台桌面端 Markdown 编辑器(含实时预览、文件操作、快捷键)
华为·electron·harmonyos
夜雨声烦丿13 小时前
Flutter 框架跨平台鸿蒙开发 - 思维导图开发教程
flutter·华为·harmonyos
2501_9445264214 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 蜘蛛纸牌游戏实现
android·java·python·flutter·游戏
IT陈图图14 小时前
基于 Flutter × OpenHarmony 开发的文本处理工具箱首页
flutter·华为·openharmony
小白阿龙14 小时前
鸿蒙+Flutter 跨平台开发——一款“随机宝盒“的开发流程
flutter·华为·harmonyos·鸿蒙
爱吃大芒果14 小时前
Flutter for OpenHarmony前置知识:Dart 语法核心知识点总结(下)
开发语言·flutter·dart