Flutter 三方库 Riverpod 的鸿蒙化实践
摘要:本文基于LabManagementSystem仓库的真实接口,探讨在Flutter for OpenHarmony中使用Riverpod进行状态管理的实践。通过分析实验室列表、详情等核心业务场景,展示Riverpod如何有效管理异步请求状态、页面缓存和错误处理。文章重点介绍模型定义、Provider分层架构以及与现有FastAPI后端的对接方案,为鸿蒙开发者提供可落地的状态管理参考方案。实践
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
Flutter Riverpod鸿蒙实践
摘要:本文基于
LabManagementSystem仓库的现有实验室预约业务,选择 pub.dev 上的flutter_riverpod作为状态管理三方库,在 Flutter for OpenHarmony 场景中实现实验室列表加载、刷新、详情切换与错误回显。文章重点放在状态管理方案如何贴合真实接口,而不是泛泛介绍框架。结论是:对于已有 Web 前端和 FastAPI 后端的项目,Riverpod 能够把 Flutter-OH 客户端中的异步请求状态、列表缓存和页面联动拆分得更清晰,适合作为鸿蒙端业务页面的基础状态层。
作者信息
- 作者:coco.D
- 背景:文章基于仓库现有
FastAPI + MySQL + Vue 3结构撰写,接口字段与业务对象均来自当前项目源码 - 适用对象:准备将现有管理系统或预约系统扩展到 OpenHarmony / HarmonyOS 客户端的开发者
选题背景
前一类鸿蒙化文章通常会聚焦网络库,比如 dio。但在真实项目里,请求只是第一步,真正影响代码可维护性的,往往是列表页面如何管理“加载中、刷新中、加载失败、详情切换、重试”等状态。
LabManagementSystem 仓库正好适合写这类内容:
- 后端接口少而清晰,便于聚焦状态管理本身
- 实验室列表和详情接口已经存在,不需要虚构业务
- Web 端已有预约页,可以为 Flutter-OH 页面拆分提供参考
因此,这篇文章换一个角度,选择 pub.dev 上的 flutter_riverpod 来做 Flutter for OpenHarmony 端的状态组织。
仓库里的真实接口长什么样
这一步很重要。状态管理如果脱离真实接口,只会写成演示代码。
已有接口
根据仓库前端 frontend/src/api/index.js 与后端 backend/app/api/routes/labs.py,当前可稳定使用的接口有:
| 接口 | 返回结构 | 用途 |
|---|---|---|
GET /api/health |
{ "status": "ok", "message": "..." } |
连通性检查 |
GET /api/labs |
{ "data": [...] } |
实验室列表 |
GET /api/labs/{id} |
单个对象 | 实验室详情 |
列表对象字段
后端对实验室对象的序列化方式如下:
def _lab_to_dict(lab: Lab):
return {
"id": lab.id,
"name": lab.name,
"location": lab.location,
"capacity": lab.capacity,
"status": lab.status,
}
这意味着 Flutter-OH 端的状态层完全可以围绕以下模型展开:
| 字段 | 说明 |
|---|---|
id |
实验室主键 |
name |
实验室名称 |
location |
房间或楼层位置 |
capacity |
可容纳人数 |
status |
当前状态 |
为什么在鸿蒙端选 Riverpod
针对这个仓库的实验室列表场景,如果只用 setState,短期内也能完成页面。但当需求增加到下面几类时,问题会很明显:
- 首次加载与下拉刷新要区分
- 列表页和详情页都依赖同一份数据
- 页面返回时希望复用已有缓存
- 失败后要支持重试,而不是让组件自己堆业务逻辑
Riverpod 在这些点上的优势比较直接:
- Provider 生命周期清晰
- 异步状态建模自然,适合 REST 接口页面
- 与 Widget 解耦,便于做单元测试
- 迁移到 Flutter for OpenHarmony 时不依赖平台专属 API,适合纯 Dart 层复用
Flutter for OpenHarmony 接入步骤
1. 添加依赖
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.6.1
dio: ^5.9.0
这里保留 dio 作为网络访问层,Riverpod 只负责状态组织。这样分层更适合后续扩展预约提交、设备申请、用户登录等模块。
2. 定义模型与仓储
class Lab {
const Lab({
required this.id,
required this.name,
required this.location,
required this.capacity,
required this.status,
});
final int id;
final String name;
final String location;
final int capacity;
final String status;
factory Lab.fromJson(Map<String, dynamic> json) {
return Lab(
id: json['id'] as int? ?? 0,
name: json['name'] as String? ?? '',
location: json['location'] as String? ?? '',
capacity: json['capacity'] as int? ?? 0,
status: json['status'] as String? ?? '',
);
}
}
class LabRepository {
LabRepository(this._dio);
final Dio _dio;
Future<List<Lab>> fetchLabs() async {
final response = await _dio.get('/api/labs');
final map = Map<String, dynamic>.from(response.data as Map);
final list = (map['data'] as List<dynamic>? ?? const []);
return list
.map((item) => Lab.fromJson(Map<String, dynamic>.from(item as Map)))
.toList();
}
Future<Lab> fetchLabDetail(int id) async {
final response = await _dio.get('/api/labs/$id');
return Lab.fromJson(
Map<String, dynamic>.from(response.data as Map),
);
}
}
这一步和仓库后端是严格对应的,没有引入额外字段,因此更适合真实迁移。
3. 定义 Provider
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dioProvider = Provider<Dio>((ref) {
return Dio(
BaseOptions(
baseUrl: 'http://192.168.1.20:8000',
connectTimeout: const Duration(seconds: 8),
receiveTimeout: const Duration(seconds: 8),
headers: const {
'Content-Type': 'application/json',
},
),
);
});
final labRepositoryProvider = Provider<LabRepository>((ref) {
return LabRepository(ref.watch(dioProvider));
});
final labListProvider = FutureProvider<List<Lab>>((ref) async {
return ref.watch(labRepositoryProvider).fetchLabs();
});
final labDetailProvider = FutureProvider.family<Lab, int>((ref, id) async {
return ref.watch(labRepositoryProvider).fetchLabDetail(id);
});
这里最关键的是 FutureProvider.family。它非常适合这个仓库里的详情接口,因为详情请求天然就是“按实验室 ID 拉取”的。
4. 列表页绑定状态
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LabListPage extends ConsumerWidget {
const LabListPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final labsAsync = ref.watch(labListProvider);
return Scaffold(
appBar: AppBar(title: const Text('实验室列表')),
body: labsAsync.when(
data: (labs) {
if (labs.isEmpty) {
return const Center(child: Text('暂无可预约实验室'));
}
return RefreshIndicator(
onRefresh: () async {
await ref.refresh(labListProvider.future);
},
child: ListView.separated(
itemCount: labs.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final lab = labs[index];
return ListTile(
title: Text(lab.name),
subtitle: Text('${lab.location} · ${lab.capacity}人'),
trailing: Text(lab.status),
onTap: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (_) => LabDetailPage(labId: lab.id),
),
);
},
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('加载失败:$error'),
const SizedBox(height: 12),
FilledButton(
onPressed: () => ref.invalidate(labListProvider),
child: const Text('重试'),
),
],
),
),
),
),
);
}
}
这个写法的好处是明显的:UI 只描述三种状态,不直接处理网络细节,后期维护比把所有逻辑堆在 StatefulWidget 中轻松得多。
5. 详情页按需加载
class LabDetailPage extends ConsumerWidget {
const LabDetailPage({super.key, required this.labId});
final int labId;
Widget build(BuildContext context, WidgetRef ref) {
final detailAsync = ref.watch(labDetailProvider(labId));
return Scaffold(
appBar: AppBar(title: const Text('实验室详情')),
body: detailAsync.when(
data: (lab) => ListView(
padding: const EdgeInsets.all(16),
children: [
ListTile(title: const Text('名称'), subtitle: Text(lab.name)),
ListTile(title: const Text('位置'), subtitle: Text(lab.location)),
ListTile(title: const Text('容量'), subtitle: Text('${lab.capacity}')),
ListTile(title: const Text('状态'), subtitle: Text(lab.status)),
],
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Text('详情加载失败:$error'),
),
),
);
}
}
详情页不依赖列表页内部状态,而是自己按 ID 获取数据,这一点和仓库当前 GET /api/labs/{id} 的设计天然匹配。
6. 应用入口
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: LabListPage(),
),
),
);
}
对于 Flutter for OpenHarmony 项目来说,ProviderScope 放在最外层是比较稳妥的组织方式,后续新增用户状态、预约筛选条件、设备分类等 Provider 时也更容易扩展。
结合仓库业务后的落地价值
1. 列表页不再依赖“页面自身记忆”
当前仓库的 Vue 端预约页把不少逻辑放在页面组件里。到了 Flutter-OH,如果依旧照搬成单页大组件,代码会很快膨胀。Riverpod 的价值在于把“实验室列表是否已加载”“是否需要刷新”“详情是否需要重新获取”这些问题从 Widget 中抽出去。
2. 接口差异可以被 Provider 层消化
这个仓库的列表接口与详情接口返回结构不同:
- 列表接口外层包了一层
data - 详情接口直接返回对象
如果没有统一的 Provider/Repository 分层,这些差异很容易散落到多个页面里。Riverpod 让这些解析逻辑集中在数据层,页面不需要重复判断。
3. 更适合后续扩展预约场景
当实验室预约提交接口、我的预约列表接口补齐后,可以继续沿用同一模式:
FutureProvider管列表拉取StateNotifier或AsyncNotifier管提交流程- 通过参数化 Provider 管理详情或筛选条件
这会比简单 setState 更适合真实项目演进。
鸿蒙化过程中容易遇到的问题
1. 刷新无效,其实是 Provider 没被刷新
不少开发者第一次用 Riverpod 时,会在下拉刷新里再次调用仓储方法,但没有刷新 Provider,本质上没有更新状态树。对于列表页,正确方式是:
await ref.refresh(labListProvider.future);
2. 真机能启动,但接口一直报超时
这类问题多半和 Riverpod 无关,而是网络地址配置不对。Flutter for OpenHarmony 在真机联调时,通常要把 baseUrl 设置成宿主机局域网 IP,不能直接写 localhost。
3. 详情页频繁重复请求
如果页面反复进出详情页,可能会出现多次拉取。此时可以根据业务需要决定是否增加缓存策略,比如在 Provider 生命周期上做进一步管理,而不是一上来就把所有请求写死在页面跳转回调里。
4. 状态管理层过早复杂化
对于当前仓库这种刚开始做鸿蒙端迁移的项目,不建议一开始就引入过多抽象。先从 Provider + Repository + Model 三层结构起步,等预约提交、登录鉴权、筛选分页真正出现后,再考虑更复杂的状态架构。
结论
如果说 dio 解决的是“怎么请求”,那么 Riverpod 解决的就是“请求回来以后,页面状态如何组织”。在 LabManagementSystem 这种已有后端接口、业务对象稳定、未来还会继续增加预约与设备模块的项目中,Riverpod 非常适合作为 Flutter for OpenHarmony 客户端的状态管理基础。
更重要的是,这种实践不依赖平台特有能力,而是建立在纯 Dart 状态建模之上。这意味着它不仅适用于 OpenHarmony / HarmonyOS,也适合未来与 Android、iOS 共用一套 Flutter 业务层代码。对跨平台框架来说,这种“多端复用同一份状态组织方式”的收益,往往比单纯把页面跑起来更重要。
参考链接
- OpenHarmony 官方文档:https://docs.openharmony.cn/
- Flutter Riverpod 包主页:https://pub.dev/packages/flutter_riverpod
- Flutter dio 包主页:https://pub.dev/packages/dio
- FastAPI 官方文档:https://fastapi.tiangolo.com/
- AtomGit 平台首页:https://atomgit.com
更多推荐



所有评论(0)