【开源鸿蒙跨平台开发先锋训练营】DAY 3 Flutter集成Dio网络请求,本地美食数据清单列表功能开发

【开源鸿蒙跨平台开发先锋训练营】Day3 Flutter集成Dio网络请求,本地美食数据清单列表功能开发

目录

[【开源鸿蒙跨平台开发先锋训练营】Day3 Flutter集成Dio网络请求,本地美食数据清单列表功能开发](#【开源鸿蒙跨平台开发先锋训练营】Day3 Flutter集成Dio网络请求,本地美食数据清单列表功能开发)

[摘 要](#摘 要)

[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.2.1 基础开发环境](#2.2.1 基础开发环境)

[2.2.2 核心依赖配置](#2.2.2 核心依赖配置)

[3 项目总体设计](#3 项目总体设计)

[3.1 架构设计](#3.1 架构设计)

[3.2 工程目录结构](#3.2 工程目录结构)

[4 网络请求能力集成](#4 网络请求能力集成)

[4.1 鸿蒙网络权限配置](#4.1 鸿蒙网络权限配置)

[4.2 美食接口配置与网络工具复用](#4.2 美食接口配置与网络工具复用)

[4.2.1 配置本地模拟美食API](#4.2.1 配置本地模拟美食API)

[4.2.2 美食接口地址配置](#4.2.2 美食接口地址配置)

[4.2.3 统一响应模型](#4.2.3 统一响应模型)

[4.2.4 网络请求封装](#4.2.4 网络请求封装)

[4.2.5 美食清单接口请求实现](#4.2.5 美食清单接口请求实现)

[5 数据清单列表页面实现](#5 数据清单列表页面实现)

[5.1 美食数据模型定义](#5.1 美食数据模型定义)

[5.2 美食清单页面UI构建](#5.2 美食清单页面UI构建)

[5.3 页面入口配置(工程集成)](#5.3 页面入口配置(工程集成))

[6 测试与验证](#6 测试与验证)

[6.1 测试环境](#6.1 测试环境)

[6.2 美食清单功能验证](#6.2 美食清单功能验证)

[6.3 问题与解决方案](#6.3 问题与解决方案)

[7 总结与展望](#7 总结与展望)

[7.1 总结](#7.1 总结)

[7.2 未来展望](#7.2 未来展望)


摘 要

为适配 OpenHarmony 生态的跨平台应用开发需求,本文目标设计实现一款本地美食清单应用。该应用基于 Flutter 框架构建,集成 http 网络请求库、图片缓存、下拉刷新及星级评分等核心功能模块,实现了美食数据的获取、解析、展示全流程。开发过程中完成了 OpenHarmony SDK 与 Flutter 环境的兼容配置,解决了第三方依赖的空安全适配问题,最终实现应用在DevEco Studio上的稳定运行。

1 引言

1.1 项目背景

Flutter是Google开发的跨平台UI框架,用于通过一套代码库高效构建跨平台应用。‌它采用Dart 编程语言驱动,支持将应用编译为原生机器代码或 JavaScript,从而实现高性能渲染和多端一致性。Flutter 的核心优势在于‌一次开发、多端部署‌,支持移动(iOS/Android)、Web、桌面(Windows/macOS/Linux)及嵌入式设备。OpenHarmony版Flutter 是Flutter针对OpenHarmony系统的适配版本,可以让你用Flutter开发HarmonyOS/OpenHarmony应用。

而本地美食清单作为高频生活类应用场景,需实现网络数据请求、图片加载优化、列表交互等核心功能,适合作为 Flutter 与 OpenHarmony 集成开发的实践载体。

1.2 项目目标

本项目旨在开发一款基于 Flutter+OpenHarmony 的跨平台本地美食清单应用,具体目标包括:

  1. 完成 Flutter 与 OpenHarmony 开发环境的搭建与兼容配置;

  2. 实现美食数据的网络请求、JSON 解析与本地模型映射;

  3. 开发具备图片缓存、下拉刷新、星级评分功能的交互界面;

  4. 确保应用在DevEco Studio(模拟器)上稳定运行。

1.3 技术选型依据

  1. 框架选择:Flutter 3.27.4(鸿蒙适配版),支持跨平台 UI 一致性渲染,适配 OpenHarmony 系统特性;

  2. 网络请求:选用 http 库(轻量高效,适配 Dart 空安全特性,满足基础数据请求需求);

  3. 功能增强:集成 cached_network_image 优化图片加载性能,pull_to_refresh 实现交互刷新,flutter_rating_bar 完成评分展示(解决原 rating_bar 库空安全不兼容问题);

  4. 开发工具:VS Code 1.108.1(Flutter 代码编写)与 DevEco Studio 6.0.0(鸿蒙权限配置与部署)协同工作。

2 项目功能与开发准备

2.1 功能核心定位

  1. 明确页面目标:

基于开源鸿蒙跨平台工程(Flutter+OpenHarmony) 开发"本地美食数据清单"页面。

  1. 核心目标:

完成工程网络请求能力的标准化集成、实现美食类数据清单的UI构建+数据绑定+交互逻辑开发、最终在DevEco Studio 6.0.0模拟器完成"网络请求→数据解析→列表渲染→交互验证"全流程运行验证,达成跨平台工程的"网络+列表"核心能力落地。

2.2 开发环境与技术栈配置

2.2.1 基础开发环境

|-----------------|--------------------------|---------------------|
| 开发工具/环境 | 版本规格 | 用途说明 |
| 操作系统 | Windows 10 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.2.2 核心依赖配置

通过VS Code软件完成pubspec.yaml 新增美食场景专属依赖。

  1. 首先在终端输入以下命令,将http package添加到依赖中:

    flutter pub add http

http package:Dart 内置的 http 包。

这是 Dart 语言标准库的一部分,提供了一个简单的 HTTP 客户端。

在 Flutter 中使用时,需要在 pubspec.yaml 文件中添加依赖:http: ^1.6.0(博主使用版本)。

该包提供了基本的 GET、POST 等请求方法,但功能相对基础。

  1. 添加"美食图片缓存"依赖(解决鸿蒙设备图片重复加载卡顿)

Vs code终端中运行以下命令:

复制代码
flutter pub add cached_network_image
  1. 添加"下拉刷新"依赖(适配鸿蒙触控交互的下拉刷新功能)

    flutter pub add pull_to_refresh

  1. 添加"美食评分组件"依赖(实现鸿蒙 UI 适配的星级评分展示)

支持空安全的评分组件:

复制代码
flutter pub add flutter_rating_bar

以上四条命令执行完成后,这些依赖会自动添加到你的pubspec.yaml的dependencies部分,同时 Flutter 会自动下载这些依赖包,无需额外执行flutter pub get(flutter pub add命令会自动触发依赖安装)。

最终你的pubspec.yaml的dependencies区域会变成这样:

依赖生效:执行 flutter pub get 命令确保无鸿蒙适配报错。

复制代码
flutter pub get

3 项目总体设计

3.1 架构设计

应用采用模块化分层架构,按功能职责划分为五层,确保代码的可维护性与扩展性:

  1. 表现层(UI 层):包含美食清单页面、列表项组件,负责用户界面渲染;

  2. 接口层(API 层):封装美食数据请求接口,统一数据获取入口;

  3. 网络层:实现 http 请求封装、响应处理、异常捕获,适配 OpenHarmony 网络特性;

  4. 数据层:定义美食数据模型,完成 JSON 与 Dart 对象的序列化 / 反序列化;

  5. 配置层:存储接口地址、超时时间等全局配置参数。

3.2 工程目录结构

复制代码
项目根目录/
├── core/            # 核心封装层
│   └── http/        # 网络请求封装
├── api/             # 接口层
├── models/          # 数据模型层
├── pages/           # 页面层
├── module.json5     # 鸿蒙权限/编译配置
└── pubspec.yaml     # 依赖配置

(详细)项目根目录/
├─ 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依赖配置

4 网络请求能力集成

4.1 鸿蒙网络权限配置

在工程ohos/entry/src/main/module.json5 的 reqPermissions 节点中声明网络权限:配置ohos.permission.INTERNET权限,确保已配置网络权限。说明鸿蒙权限机制对跨平台工程的约束逻辑。

复制代码
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]

4.2 美食接口配置与网络工具复用

完成工程目录创建(在 VSCode 的lib/下):

首先,通过VS Code软件在工程根目录的lib/文件夹下(例如博主的:D:\Flutter_HmProject\flutter_harmonyos\lib)依次创建core/、api/、models/、pages/这些目录。具体步骤如下:

在lib/目录上右键 → 点击 "新建文件夹...",依次创建以下目录:

core/目录,再在core/下创建http/子目录;

api/目录;

models/目录;

pages/目录(后续可在pages/下再建food/子目录存放美食清单页面)。

VS Code安装Flutter插件:

打开 VS Code,点击左侧边栏的"扩展"图标;

在搜索框输入"Flutter",找到官方的"Flutter"插件,点击"安装";

同样在扩展商店搜索"Dart",找到"Dart Code"插件,点击"安装"(博主在安装完Flutter插件后会自动提示安装Dart Code,确认安装即可);

安装完成后,重启 VS Code 使插件生效。

在对应目录下创建业务文件:

在刚创建的目录中,分别新建对应的 Dart 文件(右键目录→"新建文件"):

|-------------|---------------------|-----------------|
| 目录 | 新建文件名称 | 作用 |
| core/http/ | api_config.dart | 配置接口地址、超时等 |
| core/http/ | api_response.dart | 统一网络响应模型 |
| core/http/ | http_client.dart | 网络请求工具封装 |
| api/ | food_api.dart | 美食列表接口请求实现 |
| models/ | food_model.dart | 美食数据模型(JSON 解析) |
| pages/food/ | food_list_page.dart | 美食清单页面 UI + 逻辑 |

4.2.1 配置本地模拟美食API

博主使用Node.js + Express实现本地模拟美食 API。

1. 安装 Node.js(本地服务运行环境)

下载 Node.js:打开Node.js 官网,下载对应系统的 LTS 版本(Windows/Mac 都支持);

验证安装:打开终端 / 命令提示符,输入node -v和npm -v,能显示版本号即安装成功。

复制代码
node -v

npm -v

2. 创建本地接口服务项目

新建一个文件夹(比如命名为local_food_api),打开终端并进入该文件夹;

初始化项目:执行命令

复制代码
npm init -y

安装 Express(轻量 HTTP 服务框架)/安装本地接口服务需要的框架:

复制代码
npm install express

3. 编写本地接口代码

在local_food_api文件夹中新建server.js文件,编写以下代码:

复制代码
// 顶部引入cors
const cors = require('cors');
// 导入Express
const express = require('express');
const app = express();
// 配置静态资源目录:让public文件夹里的文件可以通过HTTP访问
app.use(express.static('public'));
// 启用CORS(允许所有域名访问,测试用)
app.use(cors());
// 定义端口(可自定义,比如3000)
const port = 3000;

// 模拟美食列表数据
const mockFoodData = [
  {
    id: 1,
    name: "花生松仁炒蛋",
    desc: "鸡蛋+松仁,鲜嫩可口",
    image: "/food1.jpg", // 对应public里的food1.jpg
    score: 4.8
  },
  {
    id: 2,
    name: "白玉木耳炒西蓝花",
    desc: "白玉木耳滑嫩脆弹,西蓝花清甜脆爽",
    image: "/food2.jpg",
    score: 4.9
  },
  {
    id: 3,
    name: "糖醋排骨",
    desc: "甜甜蜜蜜的糖醋排骨",
    image: "/food3.jpg",
    score: 4.5
  }
];

// 定义美食列表接口:GET请求,路径为/api/localFood/list
app.get('/api/localFood/list', (req, res) => {
  res.json(mockFoodData);
});

// 启动服务
app.listen(port, () => { // 改为用port变量
  console.log(`Server running on http://localhost:${port}`);
});

4. 启动本地接口服务

在local_food_api文件夹的终端中执行:

复制代码
node server.js

看到 "本地美食 API 服务已启动:http://localhost:3000" 即成功。

浏览器访问http://localhost:3000/api/localFood/list验证接口数据(注意:localhost也可以改为你本地的ipv4/IP地址访问)。

4.2.2 美食接口地址配置

在VS Code中创建core/http/api_config.dart文件,定义基础接口地址、超时时间与请求头,便于全局统一管理。

结合api_config.dart(URL、超时配置),封装http_client.dart:实现请求拦截、统一超时控制、错误码封装;

  1. 首先,打开 VS Code 终端,执行以下命令(将json_annotation添加到运行时依赖):

    flutter pub add json_annotation

执行后,pubspec.yaml的dependencies会新增json_annotation: ^x.x.x,解决 "json_annotation不是依赖"的错误。

  1. 在终端执行build_runner命令,生成 JSON 序列化的代码文件:

    flutter pub run build_runner build

  2. Flutter 项目中调用这个本地接口

打开 Flutter 项目的api_config.dart,修改配置:

注意:博主的IP地址为192.168.0.108,所以"static const String baseUrl = "http://192.168.0.108:3000";"中要修改为本地的IP地址。

复制代码
// core/http/api_config.dart
class ApiConfig {
  // 基础接口地址(替换为你电脑的实际IP+Node服务端口)
  // 电脑IP查看:Windows用ipconfig,Mac用ifconfig
  static const String baseUrl = "http://192.168.0.108:3000"; // 测试接口基地址
  // 美食清单数据接口(对应本地Node服务的接口路径)
  static const String food_list_url = "$baseUrl/api/localFood/list";
  // 其他配置(超时、请求头)
  static const int timeout = 10000;   // 超时时间(10s,适配鸿蒙网络特性)
  static Map<String, String> get baseHeaders {
    return {"Content-Type": "application/json;charset=UTF-8"};
  }
}

4.2.3 统一响应模型

编写core/http/api_response.dart(统一响应模型),封装接口响应格式,统一处理成功与失败状态:

基于api_response.dart定义统一响应模型:处理"成功/失败/空数据"的标准化返回格式,说明封装的价值。

复制代码
// core/http/api_response.dart
class ApiResponse<T> {
  int code;
  String msg;
  T data;
  bool success; // 实例变量

  ApiResponse({
    required this.code,
    required this.msg,
    required this.data,
    required this.success,
  });

  // 成功响应构造方法,静态方法名从"success"改为"successResponse"(避免和实例变量重名)
  static ApiResponse successResponse({dynamic data, String msg = "请求成功"}) {
    return ApiResponse(code: 200, msg: msg, data: data, success: true);
  }

  // 失败响应构造方法,失败响应的方法名也可以统一规范
  static ApiResponse errorResponse({int code = -1, String msg = "请求失败"}) {
    return ApiResponse(code: code, msg: msg, data: null, success: false);
  }
}

4.2.4 网络请求封装

编写core/http/http_client.dart(网络请求封装),基于 http 库实现 GET 请求封装,包含超时处理与异常捕获:

复制代码
// core/http/http_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'api_config.dart';
import 'api_response.dart';

class HttpClient {
  // GET请求封装
  static Future<ApiResponse> getRequest(String url) async {
    try {
      final response = await http.get(
        Uri.parse(url),
        headers: ApiConfig.baseHeaders,
      ).timeout(Duration(milliseconds: ApiConfig.timeout));

      return _handleResponse(response);
    } catch (e) {
      // 同步方法名:error → errorResponse
      return ApiResponse.errorResponse(msg: "网络异常: ${e.toString()}");
    }
  }

  // 处理响应结果
  static ApiResponse _handleResponse(http.Response response) {
    // 打印响应详情(调试用,发布时可以去掉)
    print("接口URL: ${response.request?.url}");
    print("状态码: ${response.statusCode}");
    print("响应体: ${response.body}"); // 看服务器返回的具体错误
    if (response.statusCode == 200) {
      List<dynamic> result = json.decode(response.body);
      // 同步方法名:success → successResponse
      return ApiResponse.successResponse(data: result, msg: "请求成功");
    } else {
      // 同步方法名:error → errorResponse
      return ApiResponse.errorResponse(
        code: response.statusCode,
        msg: "接口请求失败,状态码: ${response.statusCode}",
      );
    }
  }
}

4.2.5 美食清单接口请求实现

在api/目录下编写工具列表请求接口(如food_api.dart):调用封装的http_client.dart,传入api_config中的接口地址,完成请求入参、响应解析的关联。

首先,在vs code终端中输入并执行以下命令:

复制代码
flutter pub add dio:^5.0.0

美食清单接口请求实现(api/food_api.dart):

复制代码
// food_api.dart
// 先导入依赖包
import 'package:dio/dio.dart';
import '../core/http/api_config.dart'; // 对应api_config的路径

// 在food_api.dart中定义请求方法
Future<dynamic> getFoodList() async {
  try {
    Dio dio = Dio();
    print("请求接口:${ApiConfig.food_list_url}"); // 打印请求地址
    // 调用api_config中配置的接口地址
    Response response = await dio.get(ApiConfig.food_list_url);
    print("接口返回:${response.data}"); // 打印返回数据
    return response.data; // 提取接口返回的美食列表数据
  } catch (e) {
    print("接口请求失败:$e"); // 打印错误
    throw e;
  }
}

5 数据清单列表页面实现

5.1 美食数据模型定义

编写models/food_model.dart,完成美食数据模型定义:

适配美食接口的JSON格式,定义"名称/图片/评分"等核心字段,做空安全+鸿蒙数据解析适配。

先添加json_serializable依赖(VS Code终端执行),采用json_serializable实现 JSON 数据与 Dart 对象的自动转换,解决数据解析效率问题。

复制代码
flutter pub add dev:json_serializable dev:build_runner

然后编写模型代码:

复制代码
// 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(List<dynamic> json) {
    return FoodListModel(
      foodList: json.map((e) => FoodModel.fromJson(e)).toList()
    );
  }
}

在 VS Code 终端执行以下命令,自动生成food_model.g.dart解析文件:

复制代码
flutter pub run build_runner build

执行后,models/下会自动生成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,
    };

5.2 美食清单页面UI构建

美食清单页面UI构建(pages/food/food_list_page.dart):

重点适配鸿蒙设备的"图片显示/评分组件/列表布局",避免UI溢出/卡顿。

编写pages/food/food_list_page.dart(页面 UI):

复制代码
// pages/food/food_list_page.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../api/food_api.dart';
import '../../../models/food_model.dart';

class FoodListPage extends StatefulWidget {
  const FoodListPage({super.key});
  @override
  State<FoodListPage> createState() => _FoodListPageState();
}

class _FoodListPageState extends State<FoodListPage> {
  List<FoodModel> _foodList = [];
  final RefreshController _refreshController = RefreshController(initialRefresh: false);
  @override
  void initState() {
    super.initState();
    _getFoodListData(); // 初始化加载数据
  }

// 获取美食数据
Future<void> _getFoodListData() async {
  try {
    final data = await getFoodList();
    if (mounted) {
      // 先判断data是否是List类型
      if (data is List) {
        final foodListModel = FoodListModel.fromJson(data);
        setState(() {
          _foodList = foodListModel.foodList;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("美食列表加载成功")),
        );
      } else {
        throw Exception("接口返回数据不是列表");
      }
    }
  } catch (e) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("加载失败:${e.toString()}")),
      );
    }
  } finally {
    _refreshController.refreshCompleted();
  }
}

// 列表项UI构建
Widget _buildFoodItem(FoodModel food) {
  // 步骤1:新增打印,确认图片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: [
          // 步骤2:原生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),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

// 原生星星评分组件(无需第三方包)
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,
      )),
    ],
  );
}

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("本地美食清单"), centerTitle: true),
      body: SmartRefresher(
        controller: _refreshController,
        onRefresh: _getFoodListData,   // 下拉刷新
        child: _foodList.isEmpty
            ? const Center(child: CircularProgressIndicator())
            : ListView.builder(
                itemCount: _foodList.length,
                itemBuilder: (context, index) => _buildFoodItem(_foodList[index]),
              ),
      ),
    );
  }
}

5.3 页面入口配置(工程集成)

在 main.dart 中配置美食页面为入口(或路由),设置美食清单页面为应用首页,配置主题样式:

复制代码
// main.dart 美食页面入口
import 'package:flutter/material.dart';
import 'pages/food/food_list_page.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),
      home: const FoodListPage(), // 首页设置,美食清单页面
      debugShowCheckedModeBanner: false,
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

6 测试与验证

6.1 测试环境

测试设备:OpenHarmony 模拟器(API Version 20);

测试工具:DevEco Studio 6.0.0。

6.2 美食清单功能验证

  1. 页面启动验证:模拟器启动后,点击图标正常打开"本地美食清单"页面,标题栏/布局无错乱;

  2. 网络请求验证:页面自动请求美食数据,无"网络异常"提示(验证 ohos.permission.INTERNET 生效);

  3. 列表渲染验证:美食图片/名称/评分完整显示,列表滑动流畅;

  4. 交互验证:下拉列表触发刷新。

6.3 问题与解决方案

|--------------------|------------------------------|
| 遇到问题 | 解决方案 |
| rating_bar 库不支持空安全 | 替换为 flutter_rating_bar 库 |
| 图片加载卡顿 | 集成 cached_network_image 缓存组件 |
| 网络请求无响应 | 配置 OpenHarmony INTERNET 权限 |

7 总结与展望

7.1 总结

本项目成功实现了基于 Flutter+OpenHarmony 的本地美食清单跨平台应用开发,完成了从环境搭建、依赖配置、模块封装到功能测试的全流程实践。核心成果包括:

  1. 构建了兼容 OpenHarmony 的 Flutter 开发环境,解决了第三方依赖的空安全适配问题,完成了"美食场景网络请求集成+多维度列表渲染+鸿蒙设备适配";

  2. 设计了模块化的工程架构,实现了网络请求、数据解析、UI 展示的分层设计;

  3. 集成了图片缓存、下拉刷新等实用功能,提升了应用的用户体验;

  4. 验证了 Flutter 框架在 OpenHarmony 平台的兼容性与可行性。

7.2 未来展望

本项目仍有可优化与扩展的方向:

  1. 功能扩展:比如可以增加美食的分类、收藏、搜索等功能,丰富应用场景;

  2. 性能优化:引入状态管理框架(如 Provider),优化数据流转效率;

  3. 安全增强:集成 HTTPS 加密传输,提升数据传输安全性;

  4. 多端适配:优化不同尺寸鸿蒙设备的界面适配,实现 "一次开发、多端部署" 的完整跨平台体验。

最后,

欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net

相关推荐
程序员老刘·10 分钟前
Android Studio Otter 3 发布:日常开发选AS还是Cursor?
flutter·android studio·ai编程·跨平台开发·客户端开发
鸿蒙开发工程师—阿辉14 分钟前
让 AI 帮你编译部署鸿蒙应用:harmonyos-build-deploy Skill
华为·harmonyos
浩辉_16 分钟前
Dart - 内存管理与垃圾回收(GC)深度解析
flutter·dart
盐焗西兰花29 分钟前
鸿蒙学习实战之路-Reader Kit构建阅读器最佳实践
学习·华为·harmonyos
一起养小猫2 小时前
Flutter for OpenHarmony 实战:记忆棋游戏完整开发指南
flutter·游戏·harmonyos
飞羽殇情3 小时前
基于React Native鸿蒙跨平台开发构建完整电商预售系统数据模型,完成参与预售、支付尾款、商品信息展示等
react native·react.js·华为·harmonyos
Betelgeuse764 小时前
【Flutter For OpenHarmony】TechHub技术资讯界面开发
flutter·ui·华为·交互·harmonyos
铅笔侠_小龙虾4 小时前
Flutter 安装&配置
flutter
大雷神5 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地-- 第33篇:应用打包、签名与发布
华为·harmonyos
mocoding5 小时前
使用已经完成鸿蒙化适配的Flutter本地持久化存储三方库shared_preferences让你的应用能够保存用户偏好设置、缓存数据等
flutter·华为·harmonyos·鸿蒙