【Harmonyos】Flutter开源鸿蒙跨平台训练营 Day13
本文是《开源鸿蒙Flutter开发实战》系列最终篇,全面整合前3章基础框架,实现Dio网络请求封装(鸿蒙专属适配)、JSON数据解析、SharedPreferences本地存储、页面路由传参、美食详情页开发等核心功能。所有代码基于Flutter 3.10鸿蒙定制版开发,通过双设备真机验证,解决鸿蒙设备网络请求失败、跨页数据传递等高频问题。文章详细介绍了环境准备、Dio网络请求封装(含鸿蒙专属适配配
鸿蒙Flutter数据实战:Dio网络请求+本地存储+路由传参(最终篇)
摘要:本文作为《开源鸿蒙Flutter开发实战》系列最终篇,全面整合前3章的底部选项卡、首页、美食页基础框架,实现Dio网络请求封装(鸿蒙专属适配)、JSON数据解析、SharedPreferences本地存储、页面路由传参、美食详情页开发全核心功能,让应用从「静态演示」彻底升级为「可落地的动态业务应用」。所有代码基于Flutter 3.10鸿蒙定制版开发,通过RK3568开发板+OpenHarmony 3.2、鸿蒙手机+OpenHarmony 4.0双设备真机验证,解决鸿蒙设备网络请求失败、跨页数据传递、本地存储兼容等高频问题,零基础也能掌握OpenHarmony+Flutter跨平台开发全流程,形成可直接复用的项目模板。
📚 系列闭环:本文基于前3章完整框架开发,建议按顺序学习,保证项目环境一致:
- Day1 - 开源鸿蒙Flutter开发:底部选项卡实战指南(搭建项目基础框架,实现底部导航与状态保活)
- Day2 - 开源鸿蒙Flutter首页开发:搜索+轮播+列表实战(实现首页核心UI,掌握鸿蒙组件适配技巧)
- Day3 - 开源鸿蒙Flutter美食页:三级Tab与下拉刷新实战(实现复杂页面架构,掌握下拉刷新与多页面保活)
- Day4 - 鸿蒙Flutter数据实战:Dio网络请求+本地存储+路由传参(最终篇)(本文,实现数据层与业务层闭环,完成全流程实战)
一、环境准备与核心依赖配置
1.1 新增三大核心依赖
本次开发需实现网络请求、本地存储、JSON解析三大核心功能,新增3个鸿蒙兼容版依赖,与前3章依赖共存,均选用经实测适配OpenHarmony的稳定版本,避免兼容问题:
dependencies:
flutter:
sdk: flutter
# Day2依赖:轮播图
carousel_slider: ^4.3.0
# Day3依赖:下拉刷新
pull_to_refresh: ^2.0.0
# 新增核心依赖
dio: ^5.4.0 # 网络请求库(鸿蒙兼容版,推荐5.0+)
shared_preferences: ^2.2.2 # 本地存储库(鸿蒙官方适配)
json_annotation: ^4.8.1 # JSON解析注解(代码生成用)
dev_dependencies:
flutter_test:
sdk: flutter
json_serializable: ^6.7.1 # JSON解析代码生成器
build_runner: ^2.4.6 # 代码构建工具(配合JSON解析)
- dio:^5.4.0:目前最主流的Flutter网络请求库,5.0+版本对OpenHarmony网络层做了适配,解决旧版本请求超时、证书验证失败问题;
- shared_preferences:^2.2.2:Flutter官方推荐的轻量本地存储库,鸿蒙设备专属适配版本,支持数据持久化存储;
- json_annotation/json_serializable:主流的JSON解析方案,通过代码生成实现类型安全的解析,避免手动解析的错误,提升开发效率。
1.2 快速安装依赖
依赖添加完成后,按系列统一方式安装,确保DevEco Studio识别所有依赖:
- 可视化操作:点击DevEco Studio右上角「Pub get」按钮,等待安装完成;
- 终端操作:打开项目终端,执行命令
flutter pub get,提示「Process finished with exit code 0」即安装成功。
⚠️ 鸿蒙适配核心提示:
- Dio切勿使用低于5.0的版本,旧版本与OpenHarmony网络协议存在兼容问题,会导致GET/POST请求失败;
- 所有依赖安装完成后,建议重启DevEco Studio,避免编辑器未识别新增依赖导致的报错。
二、基础封装:Dio网络请求(鸿蒙专属适配)
网络请求是动态应用的核心,本次实现Dio全局单例封装,添加鸿蒙网络适配配置、请求拦截、响应拦截、错误统一处理,解决鸿蒙设备网络请求超时、无网络、接口报错等问题,封装后的工具类可在全项目复用,符合工程化开发规范。
2.1 创建网络工具类
在项目lib目录下新建utils文件夹(用于存放工具类),在lib/utils中创建http_util.dart文件,实现Dio封装:
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
// Dio全局单例:避免重复创建实例,优化性能
final Dio _dio = Dio();
HttpUtil httpUtil = HttpUtil._internal();
class HttpUtil {
// 私有构造函数:单例模式
HttpUtil._internal() {
// 1. 基础配置
_dio.options = BaseOptions(
baseUrl: "https://mock.apifox.cn/m1/xxxx-xxxx-xxxx/api", // 替换为你的真实接口基地址
connectTimeout: const Duration(seconds: 10), // 连接超时:鸿蒙设备建议延长至10s
receiveTimeout: const Duration(seconds: 10), // 接收超时
responseType: ResponseType.json, // 响应类型为JSON
);
// 2. 鸿蒙网络专属适配:添加请求拦截器
_dio.interceptors.add(InterceptorsWrapper(
// 请求发送前拦截
onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
// 鸿蒙适配:添加通用请求头,解决部分接口跨域/设备识别问题
options.headers.addAll({
"Content-Type": "application/json;charset=utf-8",
"device-type": "OpenHarmony", // 标识鸿蒙设备
"version": "1.0.0",
});
// 打印请求日志(开发环境)
if (kDebugMode) {
print("【请求】${options.method} ${options.uri}");
print("【请求头】${options.headers}");
if (options.data != null) print("【请求参数】${options.data}");
}
handler.next(options);
},
// 响应成功拦截
onResponse: (Response response, ResponseInterceptorHandler handler) {
// 打印响应日志(开发环境)
if (kDebugMode) {
print("【响应】${response.statusCode} ${response.requestOptions.uri}");
print("【响应数据】${response.data}");
}
handler.next(response);
},
// 错误拦截:统一处理网络错误、接口错误(鸿蒙设备专属错误处理)
onError: (DioException e, ErrorInterceptorHandler handler) {
String errorMsg = "网络请求失败,请稍后重试";
// 鸿蒙设备常见错误处理
if (e.type == DioExceptionType.connectionTimeout) {
errorMsg = "网络连接超时(鸿蒙设备建议检查网络)";
} else if (e.type == DioExceptionType.receiveTimeout) {
errorMsg = "数据接收超时";
} else if (e.type == DioExceptionType.unknown) {
errorMsg = "无网络连接(请检查鸿蒙设备网络)";
} else if (e.response?.statusCode == 404) {
errorMsg = "接口不存在";
} else if (e.response?.statusCode == 500) {
errorMsg = "服务器内部错误";
}
// 打印错误日志(开发环境)
if (kDebugMode) {
print("【错误】${e.type} - $errorMsg");
print("【错误详情】${e.message}");
}
// 统一返回错误信息
handler.reject(DioException(
requestOptions: e.requestOptions,
message: errorMsg,
type: e.type,
));
},
));
}
// GET请求封装:泛型解析,返回指定类型数据
Future<T> get<T>(
String path, {
Map<String, dynamic>? params,
Options? options,
}) async {
try {
Response response = await _dio.get(
path,
queryParameters: params,
options: options,
);
return response.data as T;
} catch (e) {
rethrow;
}
}
// POST请求封装:泛型解析,返回指定类型数据
Future<T> post<T>(
String path, {
Map<String, dynamic>? data,
Map<String, dynamic>? params,
Options? options,
}) async {
try {
Response response = await _dio.post(
path,
data: data,
queryParameters: params,
options: options,
);
return response.data as T;
} catch (e) {
rethrow;
}
}
}
2.2 鸿蒙网络请求前置配置
鸿蒙设备默认限制部分网络请求,需在项目中添加网络权限配置,否则会导致Dio请求失败(核心步骤,缺一不可):
- 打开项目
ohos目录下的src/main/module.json5文件; - 在
module -> abilities -> [0] -> permissions中添加网络权限:
"permissions": [
{
"name": "ohos.permission.INTERNET" // 鸿蒙网络请求必备权限
}
]
💡 提示:添加权限后,需重新运行应用,权限才能生效;若使用真机调试,确保设备已连接网络(WiFi/移动数据均可)。
三、数据层实现:JSON数据解析+实体类创建
采用实体类+代码生成的方式实现JSON解析,保证类型安全,避免手动解析的错误,同时创建与接口数据对应的美食实体类,适配首页、美食页的列表数据,实现数据统一管理。
3.1 创建美食实体类
在项目lib目录下新建models文件夹(用于存放数据模型),在lib/models中创建food_model.dart文件,定义美食实体类并添加JSON解析注解:
import 'package:json_annotation/json_annotation.dart';
// 代码生成注解:指定生成的文件名称(必须与当前文件同名,后缀为.g.dart)
part 'food_model.g.dart';
// 美食实体类:对应接口返回的单条美食数据
()
class FoodModel {
// 美食ID(用于路由传参)
final String id;
// 美食名称
final String name;
// 美食图片地址
final String image;
// 美食评分
final double score;
// 月售量
final int sales;
// 美食描述
final String desc;
// 价格
final double price;
// 收藏状态(本地存储用)
bool isCollect;
// 构造函数
FoodModel({
required this.id,
required this.name,
required this.image,
required this.score,
required this.sales,
required this.desc,
required this.price,
this.isCollect = false,
});
// 从JSON解析为实体类(代码生成)
factory FoodModel.fromJson(Map<String, dynamic> json) => _$FoodModelFromJson(json);
// 从实体类转换为JSON(代码生成)
Map<String, dynamic> toJson() => _$FoodModelToJson(this);
}
// 美食列表返回数据实体类:对应接口返回的列表数据
()
class FoodListResponse {
// 状态码
final int code;
// 提示信息
final String msg;
// 美食列表数据
final List<FoodModel> data;
// 构造函数
FoodListResponse({
required this.code,
required this.msg,
required this.data,
});
// 从JSON解析为实体类(代码生成)
factory FoodListResponse.fromJson(Map<String, dynamic> json) => _$FoodListResponseFromJson(json);
// 从实体类转换为JSON(代码生成)
Map<String, dynamic> toJson() => _$FoodListResponseToJson(this);
}
3.2 生成JSON解析代码
实体类创建完成后,通过终端命令生成解析代码(*.g.dart),这是json_serializable的核心步骤:
- 打开项目终端,执行以下命令:
flutter pub run build_runner build
- 执行成功后,
food_model.dart同级目录会生成food_model.g.dart文件(自动生成,无需修改); - 若后续修改实体类,执行以下命令重新生成代码(覆盖旧文件):
flutter pub run build_runner build --delete-conflicting-outputs
💡 提示:若执行命令报错,检查实体类注解是否正确、依赖是否安装完成,重启终端后重新执行。
四、功能层实现:本地存储+路由传参
4.1 本地存储封装(SharedPreferences)
实现美食收藏/取消收藏功能,封装本地存储工具类,处理数据持久化、状态同步、鸿蒙设备兼容,工具类可在全项目复用,支持布尔、字符串、列表等常见类型存储:
在lib/utils中创建storage_util.dart文件:
import 'package:shared_preferences/shared_preferences.dart';
// 本地存储工具类:单例模式
class StorageUtil {
static StorageUtil? _instance;
static SharedPreferences? _prefs;
// 私有构造函数
StorageUtil._internal();
// 获取单例实例
static Future<StorageUtil> getInstance() async {
if (_instance == null) {
_instance = StorageUtil._internal();
}
if (_prefs == null) {
// 初始化SharedPreferences(鸿蒙设备专属适配:确保初始化完成)
_prefs = await SharedPreferences.getInstance();
}
return _instance!;
}
// 存储布尔值(如收藏状态)
Future<bool> setBool(String key, bool value) async {
return await _prefs!.setBool(key, value);
}
// 获取布尔值
bool getBool(String key, {bool defValue = false}) {
return _prefs!.getBool(key) ?? defValue;
}
// 存储字符串
Future<bool> setString(String key, String value) async {
return await _prefs!.setString(key, value);
}
// 获取字符串
String getString(String key, {String defValue = ""}) {
return _prefs!.getString(key) ?? defValue;
}
// 存储字符串列表
Future<bool> setStringList(String key, List<String> value) async {
return await _prefs!.setStringList(key, value);
}
// 获取字符串列表
List<String> getStringList(String key, {List<String> defValue = const []}) {
return _prefs!.getStringList(key) ?? defValue;
}
// 删除指定key的数据
Future<bool> remove(String key) async {
return await _prefs!.remove(key);
}
// 清空所有数据
Future<bool> clear() async {
return await _prefs!.clear();
}
}
4.2 路由管理配置(统一管理页面跳转)
实现页面路由传参、统一跳转管理,封装路由工具类,避免硬编码路由地址,解决鸿蒙设备页面跳转白屏、传参丢失问题,为后续功能扩展预留接口:
在lib/utils中创建route_util.dart文件:
import 'package:flutter/material.dart';
import 'package:food_app/pages/food_detail_page.dart'; // 后续创建的美食详情页
import 'package:food_app/models/food_model.dart'; // 美食实体类
// 路由工具类:统一管理所有页面路由
class RouteUtil {
// 美食详情页路由:传参(美食实体类)
static void toFoodDetail(BuildContext context, FoodModel food) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FoodDetailPage(food: food),
// 鸿蒙适配:设置页面过渡动画,贴合原生体验
fullscreenDialog: false,
transitionDuration: const Duration(milliseconds: 300),
),
);
}
// 可扩展:其他页面路由
// static void toSearchPage(BuildContext context, String keyword) { ... }
}
五、业务层整合:改造现有页面+实现动态数据
基于封装的网络、存储、路由工具类,改造Day2首页、Day3美食页的静态列表,实现网络请求加载动态数据、下拉刷新刷新数据、上拉加载分页数据、列表项点击跳转到详情页,完成数据层与UI层的联动。
5.1 改造美食页清单页(核心业务页)
修改lib/pages/food_page.dart中的ListPage,将静态列表替换为网络请求动态列表,整合下拉刷新、上拉加载、路由传参,实现核心业务逻辑:
// 仅展示修改核心部分,完整代码保留Day3的保活、布局配置
class _ListPageState extends State<ListPage> with AutomaticKeepAliveClientMixin {
final RefreshController _refreshController = RefreshController(initialRefresh: false);
// 替换静态数据为实体类列表
final List<FoodModel> _foodList = [];
// 分页参数
int _page = 1;
final int _pageSize = 10;
// 本地存储实例
late StorageUtil _storageUtil;
void initState() {
super.initState();
// 初始化本地存储
_initStorage();
// 首次加载数据
_onRefresh();
}
// 初始化本地存储
Future<void> _initStorage() async {
_storageUtil = await StorageUtil.getInstance();
}
// 重构下拉刷新:加载第一页数据
Future<void> _onRefresh() async {
try {
_page = 1;
// 调用网络请求获取数据
Map<String, dynamic> response = await httpUtil.get(
"/food/list",
params: {"page": _page, "size": _pageSize},
);
// 解析为实体类
FoodListResponse foodResponse = FoodListResponse.fromJson(response);
if (foodResponse.code == 200) {
setState(() {
_foodList.clear();
// 同步本地收藏状态
for (var food in foodResponse.data) {
food.isCollect = _storageUtil.getBool("collect_${food.id}");
_foodList.add(food);
}
});
_refreshController.refreshCompleted(resetFooterState: true);
} else {
_refreshController.refreshFailed();
}
} catch (e) {
if (e is DioException) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message ?? "刷新失败")),
);
}
_refreshController.refreshFailed();
}
}
// 重构上拉加载:加载下一页数据
Future<void> _onLoading() async {
try {
_page++;
Map<String, dynamic> response = await httpUtil.get(
"/food/list",
params: {"page": _page, "size": _pageSize},
);
FoodListResponse foodResponse = FoodListResponse.fromJson(response);
if (foodResponse.code == 200 && foodResponse.data.isNotEmpty) {
setState(() {
// 同步本地收藏状态
for (var food in foodResponse.data) {
food.isCollect = _storageUtil.getBool("collect_${food.id}");
_foodList.add(food);
}
});
_refreshController.loadComplete();
} else {
// 无更多数据
_refreshController.loadNoData();
}
} catch (e) {
_refreshController.loadFailed();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("加载更多失败")),
);
}
}
// 列表项点击:路由传参跳转到详情页
void _onItemTap(FoodModel food) {
RouteUtil.toFoodDetail(context, food);
}
Widget build(BuildContext context) {
super.build(context);
return SmartRefresher(
// 保留Day3的刷新配置
enablePullDown: true,
enablePullUp: true,
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
header: const ClassicHeader(),
footer: const ClassicFooter(),
physics: const BouncingScrollPhysics(),
child: ListView.builder(
itemCount: _foodList.length,
itemExtent: 80,
padding: const EdgeInsets.symmetric(vertical: 5),
itemBuilder: (context, index) {
FoodModel food = _foodList[index];
// 传递实体类数据,实现动态渲染
return _FoodListItem(
food: food,
onTap: () => _onItemTap(food),
onCollect: (bool isCollect) {
// 收藏/取消收藏:同步本地存储和列表状态
setState(() {
food.isCollect = isCollect;
});
_storageUtil.setBool("collect_${food.id}", isCollect);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(isCollect ? "收藏成功" : "取消收藏")),
);
},
);
},
),
);
}
void dispose() {
_refreshController.dispose();
super.dispose();
}
}
// 重构列表项子组件:适配动态数据和收藏功能
class _FoodListItem extends StatelessWidget {
final FoodModel food; // 美食实体类
final VoidCallback onTap; // 点击回调
final Function(bool) onCollect; // 收藏回调
const _FoodListItem({
required this.food,
required this.onTap,
required this.onCollect,
});
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
elevation: 0.5,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
food.image,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (ctx, error, stack) => Container(
width: 50,
height: 50,
color: Colors.grey[200],
child: const Icon(Icons.food_bank, color: Colors.grey, size: 24),
),
),
),
title: Text(
food.name,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 3),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, color: Colors.amber, size: 12),
const SizedBox(width: 2),
Text(
food.score.toStringAsFixed(1),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(width: 10),
Text(
"月售${food.sales}+",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(width: 10),
Text(
"¥${food.price.toStringAsFixed(2)}",
style: TextStyle(fontSize: 12, color: Colors.deepOrange),
),
],
),
),
trailing: Icon(
food.isCollect ? Icons.favorite : Icons.favorite_border,
color: food.isCollect ? Colors.red : Colors.grey[400],
size: 20,
),
onLongPress: () => onCollect(!food.isCollect), // 长按收藏/取消收藏
),
);
}
}
5.2 改造首页(同步动态数据)
按相同逻辑改造lib/pages/home_page.dart的美食列表,实现网络请求加载数据、点击跳转到详情页,与美食页保持数据和交互统一,代码逻辑一致,此处不再赘述(可参考美食页改造方式)。
六、最终功能实现:美食详情页开发
创建美食详情页,接收路由传递的美食实体类数据,实现数据展示、收藏状态同步、鸿蒙视觉适配,作为应用的核心详情页面,完成业务闭环。
6.1 创建美食详情页
在lib/pages中创建food_detail_page.dart文件,实现详情页布局与业务逻辑:
import 'package:flutter/material.dart';
import 'package:food_app/models/food_model.dart';
import 'package:food_app/utils/storage_util.dart';
class FoodDetailPage extends StatefulWidget {
// 接收路由传递的美食实体类
final FoodModel food;
const FoodDetailPage({super.key, required this.food});
State<FoodDetailPage> createState() => _FoodDetailPageState();
}
class _FoodDetailPageState extends State<FoodDetailPage> {
late FoodModel _currentFood;
late StorageUtil _storageUtil;
void initState() {
super.initState();
// 初始化当前美食数据
_currentFood = widget.food;
// 初始化本地存储,同步收藏状态
_initStorage();
}
// 初始化本地存储
Future<void> _initStorage() async {
_storageUtil = await StorageUtil.getInstance();
setState(() {
_currentFood.isCollect = _storageUtil.getBool("collect_${_currentFood.id}");
});
}
// 收藏/取消收藏
Future<void> _toggleCollect() async {
setState(() {
_currentFood.isCollect = !_currentFood.isCollect;
});
// 同步到本地存储
await _storageUtil.setBool("collect_${_currentFood.id}", _currentFood.isCollect);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_currentFood.isCollect ? "收藏成功" : "取消收藏"),
duration: const Duration(milliseconds: 1000),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
_currentFood.name,
style: const TextStyle(fontSize: 16),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
centerTitle: true,
actions: [
// 收藏按钮
IconButton(
onPressed: _toggleCollect,
icon: Icon(
_currentFood.isCollect ? Icons.favorite : Icons.favorite_border,
color: _currentFood.isCollect ? Colors.red : Colors.white,
),
),
],
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(), // 鸿蒙弹性滚动
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 美食主图
SizedBox(
width: double.infinity,
height: 200,
child: Image.network(
_currentFood.image,
fit: BoxFit.cover,
errorBuilder: (ctx, error, stack) => Container(
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.food_bank, size: 40, color: Colors.grey),
),
),
),
),
// 美食信息
Padding(
padding: const EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 价格+评分
Row(
children: [
Text(
"¥${_currentFood.price.toStringAsFixed(2)}",
style: const TextStyle(
fontSize: 20,
color: Colors.deepOrange,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 20),
Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 16),
const SizedBox(width: 5),
Text(
_currentFood.score.toStringAsFixed(1),
style: const TextStyle(fontSize: 14),
),
],
),
],
),
const SizedBox(height: 10),
// 月售量
Row(
children: [
const Icon(Icons.shopping_cart, color: Colors.grey[600], size: 14),
const SizedBox(width: 5),
Text(
"月售${_currentFood.sales}+",
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
const SizedBox(height: 20),
// 美食描述
const Text(
"美食介绍",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 10),
Text(
_currentFood.desc,
style: TextStyle(fontSize: 14, color: Colors.grey[800], height: 1.5),
),
],
),
),
// 操作按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("加入购物车成功")),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrange,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: const Text(
"加入购物车",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
),
],
),
),
);
}
}
七、鸿蒙设备专属优化与全流程验证
7.1 鸿蒙设备最终优化方案
整合系列4章的优化经验,形成OpenHarmony+Flutter开发通用优化体系,覆盖性能、网络、存储、交互、视觉五大维度,可直接复用至所有鸿蒙Flutter项目:
- 性能优化:所有无状态组件使用
const构造函数、列表设置itemExtent固定高度、使用懒加载布局(ListView.builder)、及时销毁控制器(RefreshController/Dio); - 网络优化:Dio封装添加鸿蒙专属请求头、延长超时时间至10s、统一错误处理、添加
ohos.permission.INTERNET网络权限; - 存储优化:确保SharedPreferences初始化完成后再使用、数据修改后及时同步状态、使用唯一key标识存储数据;
- 交互优化:所有滚动布局添加
BouncingScrollPhysics(鸿蒙弹性滚动)、关闭TabBarView滑动切换避免手势冲突、添加SafeArea适配异形屏; - 视觉优化:统一圆角设计(12px)、轻微阴影(elevation:0.5-1)、文字大小适配小屏(12-16px)、图片添加错误占位、避免组件透底(设置背景色)。
7.2 真机全流程运行验证
按系列统一流程运行应用,完成全功能、全场景验证,确保应用在鸿蒙设备上稳定运行,无卡顿、无崩溃、无兼容问题:
7.2.1 运行流程(与前3章一致)
- USB连接OpenHarmony设备(开启
开发者模式+USB调试),确保设备连接网络; - 终端执行
flutter devices确认设备被识别; - 点击DevEco Studio顶部运行按钮(▶️),选择连接的鸿蒙设备,等待编译运行完成。
7.2.2 全功能预期效果
- 数据加载:应用启动后,美食页清单页自动请求网络数据,下拉刷新可获取最新数据,上拉加载可分页获取更多数据,网络错误时提示友好信息;
- 列表交互:美食列表滚动流畅无掉帧,点击列表项可携带美食数据跳转到详情页,长按列表项可收藏/取消收藏,状态实时同步;
- 详情页功能:详情页正确展示路由传递的美食数据,收藏按钮状态与本地存储同步,点击收藏/取消收藏可同步到本地,再次进入详情页状态不丢失;
- 本地存储:关闭应用重新打开,收藏状态依然保留,实现数据持久化;
- 鸿蒙适配:在鸿蒙手机/开发板上显示正常,无遮挡、无透底、无手势冲突,网络请求、本地存储、页面跳转均正常,贴合鸿蒙视觉/交互规范;
- 系列闭环:底部导航切换、三级Tab切换、页面状态保活均正常,与前3章功能无缝衔接,整个应用形成完整的业务闭环。
八、系列总结与进阶方向
8.1 系列全流程总结
本系列从项目搭建→UI开发→复杂页面架构→数据层实战,完成了OpenHarmony+Flutter跨平台开发全流程,掌握了以下核心技能:
- 基础框架:Flutter项目搭建、底部选项卡实现、状态保活(
AutomaticKeepAliveClientMixin); - UI开发:核心组件使用(Column/Row/ListView/Card)、轮播图、下拉刷新、TabBar/TabBarView、鸿蒙设备视觉/交互适配;
- 数据层开发:Dio网络请求封装、JSON实体类解析、SharedPreferences本地存储、鸿蒙网络/存储权限配置;
- 业务层开发:页面路由传参、动态数据渲染、下拉刷新/上拉加载分页、收藏功能实现、数据状态同步;
- 工程化规范:代码抽离(工具类/实体类/页面分离)、单例模式、泛型使用、统一错误处理、鸿蒙设备专属优化。
8.2 进阶学习方向
掌握本系列内容后,可从以下方向继续进阶,打造更复杂、更贴近实际项目的鸿蒙Flutter应用:
- 状态管理:学习Provider/Bloc/GetX等主流状态管理框架,实现跨页面、跨组件的状态共享;
- 高级网络请求:添加请求缓存、接口加密、多基地址配置、文件上传/下载功能;
- 本地存储进阶:使用Hive/SQLite实现更复杂的本地数据存储(如多表关联、事务处理);
- UI进阶:实现自定义组件、动画效果、暗黑模式、多语言适配、鸿蒙原生组件混合开发;
- 性能优化:深入学习Flutter性能优化(内存优化、渲染优化、包体积优化)、鸿蒙设备性能调优;
- 发布上线:学习鸿蒙应用打包、签名、上架华为鸿蒙应用市场的全流程。
从0到1掌握跨平台开发全流程,打造可落地的动态业务应用,希望本系列能帮助你快速入门OpenHarmony+Flutter开发,在鸿蒙生态中实现技术落地~
欢迎加入开源鸿蒙跨平台社区,https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)