【Flutter For OpenHarmony第三方库】Flutter 状态管理方案 Riverpod 的鸿蒙化适配实践
第一,状态类的设计要充分考虑不可变性。每次状态更新都应生成新的状态对象,而非在原对象上修改。这种设计虽然增加了些许代码量,但带来的可追溯性与可测试性提升是值得的。当状态变更出现异常时,不可变设计可以轻松通过打印日志定位问题。此外,不可变状态也天然支持撤销/重做功能的实现。第二,Notifier 内部的逻辑要保持精简。将复杂的计算逻辑分散到派生 Provider 中,可以保持 Notifier 的清
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.netOpenHarmony
Flutter 状态管理方案 Riverpod 的鸿蒙化适配实践
引言
在 Flutter 跨平台应用开发领域,状态管理始终是架构设计的核心议题。从早期的 BLoC 模式到 Redux 思想在 Flutter 中的应用,再到 Provider、GetX、Riverpod 等方案的出现,Flutter 开发者面对的状态管理选择日益丰富。然而,当 OpenHarmony 平台逐渐走入开发者的视野时,一个关键问题浮出水面:现有的状态管理方案能否平滑迁移到这一新兴平台?本文将详细介绍如何将 Flutter 生态中备受推崇的状态管理库 Riverpod 适配到 OpenHarmony 平台,重点阐述 AsyncNotifierProvider 在鸿蒙网络请求场景中的实际应用,以及与现有 Provider 代码的渐进式迁移策略。
Riverpod 作为 Provider 的进阶方案,由 Dart 社区知名开发者 Remi Rousselet 设计,其设计理念融合了函数式编程与依赖注入的最佳实践。Riverpod 的核心优势在于:编译时类型安全、声明式的 Provider 组合、与 BuildContext 完全解耦的架构设计、强大的异步状态管理能力。这些特性使得 Riverpod 特别适合构建复杂的企业级应用。对于面向 OpenHarmony 平台的 Flutter 应用而言,Riverpod 的纯 Dart 实现特性使其具备天然的跨平台兼容性优势,无需任何平台特定适配即可在鸿蒙设备上运行。
本文将按照技术选型、方案设计、实现细节、测试验证的逻辑展开,帮助读者全面理解 Riverpod 在 OpenHarmony 平台上的适配过程。读者不仅能够了解适配的技术细节,还能掌握迁移的最佳实践,为自己的项目迁移工作提供参考。
一、需求分析与技术选型
1.1 现有架构的审视与痛点识别
在本次适配工作开展之前,项目采用 Provider 作为主要的状态管理方案。Provider 在 Flutter 生态中的普及度很高,其简单易用的特点降低了状态管理的入门门槛。项目中的 Provider 使用主要分布在四个业务模块:帖子列表管理(PostProvider)、应用设置管理(SettingsProvider)、待办事项管理(TodoProvider)以及消息通知管理(MessageProvider)。
以 PostProvider 为例,其核心职责包括:帖子列表的加载与缓存、用户筛选功能的实现、加载状态与错误信息的维护。以下是原有 Provider 的实现结构:
class PostProvider with ChangeNotifier {
final PostService _postService = PostService();
List<Post> _posts = [];
bool _isLoading = false;
String? _errorMessage;
int _selectedPostId = 0;
List<Post> get posts => _posts;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
int get selectedPostId => _selectedPostId;
Future<void> loadPosts({int limit = 20}) async {
setLoading(true);
_errorMessage = null;
try {
final posts = await _postService.getPosts(limit: limit);
_posts = posts.map((p) => p.copyWithImage(
Post.generateImageUrl(p.id),
Post.generateAvatarUrl(p.userId),
)).toList();
} catch (e) {
_errorMessage = e.toString();
} finally {
setLoading(false);
}
}
void setLoading(bool value) {
_isLoading = value;
notifyListeners();
}
void clearPosts() {
_posts = [];
notifyListeners();
}
}
这种实现模式存在几个值得改进的地方。首先,isLoading、errorMessage 等状态字段需要在每个 Provider 中重复定义,增加了代码冗余。其次,ChangeNotifier 的 build 方法无法执行异步操作,这意味着初始化逻辑必须放在页面的 initState 中处理,增加了页面代码的复杂度。再次,由于 Provider 依赖运行时类型检查,类型错误只能在运行阶段被发现,影响开发效率。最后,Provider 的测试需要构建完整的 Widget 树,无法进行纯粹的单元测试。
1.2 Riverpod 核心特性与技术优势
Riverpod 相比 Provider 在多个维度实现了显著提升,这些提升对于构建可维护、可测试的应用至关重要。
编译时类型安全是 Riverpod 最重要的改进之一。在 Riverpod 中,每个 Provider 都有明确的类型声明,IDE 可以在编译阶段提供准确的代码补全和错误提示。当开发者尝试将错误类型的值赋给 Provider 时,编译器会立即报错,而非等到运行时才暴露问题。这种设计大幅缩短了问题发现周期,提升了开发效率。在大型团队协作中,这种编译时检查可以有效避免因类型误用导致的线上问题。
声明式依赖注入是 Riverpod 的另一核心优势。在 Riverpod 中,一个 Provider 可以声明对其他 Provider 的依赖,这种依赖关系以声明式的方式表达,而非传统的构造函数注入。Riverpod 会自动处理依赖的初始化顺序,确保每个 Provider 在被使用时其依赖项已经准备就绪。这种设计使得服务的管理与替换变得异常灵活,特别适合需要注入 mock 服务的测试场景。
// 服务 Provider
final postServiceProvider = Provider<PostService>((ref) {
return PostService();
});
// 使用服务的 Notifier
class PostNotifier extends Notifier<List<Post>> {
late final PostService _service;
List<Post> build() {
_service = ref.watch(postServiceProvider);
return [];
}
Future<void> loadPosts() async {
final posts = await _service.getPosts();
state = posts;
}
}
与 BuildContext 完全解耦是 Riverpod 架构设计的关键决策。在 Provider 方案中,Provider 的访问需要通过 BuildContext,这导致测试代码必须构建完整的 Widget 树才能访问 Provider。Riverpod 提供了独立的 ProviderContainer,允许在完全脱离 Widget 环境的情况下测试 Provider 逻辑。这种设计使得单元测试的编写变得简单可靠。
// Riverpod 测试示例
test('PostNotifier should load posts', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
final notifier = container.read(postProvider.notifier);
await notifier.loadPosts();
final state = container.read(postProvider);
expect(state.posts, isNotEmpty);
});
AsyncNotifierProvider 对异步操作的原生支持是本文关注的重点。移动应用中最常见的异步操作是网络请求。传统的处理方式需要在状态类中维护 isLoading、errorMessage 等字段,逻辑分散且容易出错。Riverpod 提供的 AsyncNotifierProvider 通过 AsyncValue 包装机制,实现了异步状态的标准化管理,使得加载中、成功、错误三种状态的切换变得优雅简洁。
1.3 适配 OpenHarmony 的可行性论证
Riverpod 完全基于 Dart 语言实现,不包含任何平台特定代码或原生插件依赖。这一特性决定了 Riverpod 在理论层面对所有支持 Flutter 的平台都具有兼容性,包括 OpenHarmony。Flutter 引擎负责将 Dart 代码编译为各平台的原生指令,而 Riverpod 的所有逻辑都运行在 Dart 层,与平台特定的实现完全无关。
项目的适配验证工作重点关注两个方面:核心状态管理功能的稳定性,以及 AsyncNotifierProvider 在鸿蒙网络请求场景中的表现。稳定性验证关注点包括:状态更新的实时性、内存泄漏的可能性、异常恢复能力等。异步数据流验证关注点包括:网络请求的发起与响应、加载状态的正确展示、错误处理的完整性等。
经过实际项目验证,Riverpod 在 OpenHarmony Flutter 引擎上的表现与 Android 平台基本一致。核心功能包括状态监听、异步处理、Provider 派生等均工作正常,未发现任何平台相关的兼容性问题。以下将详细阐述具体的适配实现过程与验证结果。
二、渐进式迁移方案的设计与实现
2.1 迁移策略的制定
考虑到项目已具备一定规模,状态管理方案的切换需要遵循渐进式原则,避免对现有功能造成冲击。本次迁移采用"并行共存、逐步替换"的策略,具体原则如下:
第一,新旧代码并行存在。 原有的 Provider 代码保留不动,新编写的 Riverpod 代码与之并行存在。这种设计确保了迁移过程完全可控,任何时候都可以回滚到原有的 Provider 方案。在团队协作中,这种策略也降低了沟通成本,不需要所有开发者同时完成迁移。
第二,按模块逐步迁移。 业务模块按优先级排序,逐个完成迁移。每完成一个模块的迁移都进行充分的功能验证,确保迁移质量。建议从相对简单的模块开始,如 Settings 模块,逐步过渡到复杂的模块,如 Post 模块。
第三,文件命名保持一致。 为每个业务模块创建对应的 Riverpod 实现文件时,保持文件命名与原有 Provider 一致,仅在文件名后缀或目录结构上区分。这种设计便于后续代码维护,降低了团队成员的学习成本。
第四,统一导出管理。 在 providers.dart 统一导出文件中同时包含新旧两种实现,页面代码可以根据实际情况灵活选择使用哪种方案。这种设计支持平滑过渡,避免一次性全面替换带来的风险。
具体的目录结构规划如下:
lib/providers/
├── post_provider.dart # 原有 Provider(保留)
├── post_riverpod.dart # Riverpod 实现(新增)
├── settings_provider.dart # 原有 Provider(保留)
├── settings_riverpod.dart # Riverpod 实现(新增)
├── todo_provider.dart # 原有 Provider(保留)
├── todo_riverpod.dart # Riverpod 实现(新增)
├── message_provider.dart # 原有 Provider(保留)
├── message_riverpod.dart # Riverpod 实现(新增)
└── providers.dart # 统一导出
2.2 状态类的规范化设计
在 Riverpod 架构中,状态类负责数据的结构化存储,类似于传统架构中的 Model 层。一个设计良好的状态类应当具备以下特征:不可变属性(使用 final 修饰)、完整的拷贝方法(copyWith)、合理的默认值设定。状态类的设计借鉴了函数式编程中不可变数据结构的思想。
以下是我们设计的 PostState 状态类:
class PostState {
final List<Post> posts;
final bool isLoading;
final String? errorMessage;
final int selectedPostId;
const PostState({
this.posts = const [],
this.isLoading = false,
this.errorMessage,
this.selectedPostId = 0,
});
PostState copyWith({
List<Post>? posts,
bool? isLoading,
String? errorMessage,
int? selectedPostId,
}) {
return PostState(
posts: posts ?? this.posts,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
selectedPostId: selectedPostId ?? this.selectedPostId,
);
}
}
这种设计模式的关键优势在于不可变性。每次状态变更都生成新的状态对象,而非在原对象上修改。这种设计带来的好处是多方面的:状态变更可追溯、状态历史可回放、状态比较更简单、并发场景更安全。对于需要实现撤销/重做功能的场景,不可变状态的设计可以大大简化实现复杂度。
copyWith 方法的实现采用命名参数模式,每个参数都有默认值(当前值),调用者只需指定需要变更的字段即可。这种模式既保证了类型安全,又提供了良好的使用体验。与直接修改属性相比,copyWith 方法的优势在于:调用点明确展示了哪些字段发生了变化,便于代码审查。
2.3 Notifier 类的实现规范
Notifier 是 Riverpod 中处理业务逻辑的核心类,负责状态的读取、修改与业务方法的实现。一个标准的 Notifier 需要继承自 Notifier,其中 T 是对应的状态类型。以下是 PostNotifier 的完整实现:
class PostNotifier extends Notifier<PostState> {
PostState build() => const PostState();
final PostService _postService = PostService();
Future<void> loadPosts({int limit = 20}) async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
final posts = await _postService.getPosts(limit: limit);
final enhancedPosts = posts.map((p) => p.copyWithImage(
Post.generateImageUrl(p.id),
Post.generateAvatarUrl(p.userId),
)).toList();
state = state.copyWith(posts: enhancedPosts, isLoading: false);
} catch (e) {
state = state.copyWith(errorMessage: e.toString(), isLoading: false);
}
}
Future<void> loadPostsByUser(int userId) async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
final posts = await _postService.getPostsByUser(userId);
final enhancedPosts = posts.map((p) => p.copyWithImage(
Post.generateImageUrl(p.id),
Post.generateAvatarUrl(p.userId),
)).toList();
state = state.copyWith(posts: enhancedPosts, isLoading: false);
} catch (e) {
state = state.copyWith(errorMessage: e.toString(), isLoading: false);
}
}
Future<Post?> loadPostById(int id) async {
state = state.copyWith(selectedPostId: id);
try {
return await _postService.getPostById(id);
} catch (e) {
state = state.copyWith(errorMessage: e.toString());
return null;
}
}
void clearPosts() {
state = state.copyWith(posts: []);
}
void clearError() {
state = PostState(
posts: state.posts,
isLoading: state.isLoading,
errorMessage: null,
selectedPostId: state.selectedPostId,
);
}
}
Notifier 类的实现有几个关键要点需要把握。第一,build 方法返回初始状态,该方法仅在 Provider 首次被访问时执行一次,用于初始化 Notifier 实例。与 ChangeNotifier 不同,build 方法可以返回需要复杂初始化的状态对象。第二,业务方法的实现中通过修改 state 属性触发状态更新,Riverpod 会自动进行必要的 UI 刷新。第三,将服务实例化放在 Notifier 内部而非 build 方法中,可以避免不必要的重复初始化。
2.4 Provider 的声明与派生
Provider 是 Riverpod 暴露给外部访问的入口点,其声明方式直接影响使用体验。以下是基础 Provider 的声明方式:
final postProvider = NotifierProvider<PostNotifier, PostState>(() {
return PostNotifier();
});
这种声明方式的优势在于类型推导清晰。PostNotifier 明确了业务逻辑的处理类,PostState 明确了管理的数据类型,外部使用时 IDE 可以准确提供代码补全。结合 Dart 的空安全特性,类型错误在编译阶段即可被发现。
除了基础 Provider,我们还设计了多个派生 Provider 用于满足不同的业务场景:
/// 选中帖子 ID 的简单状态 Provider
final selectedPostIdProvider = StateProvider<int>((ref) => 0);
/// 派生 Provider - 基于 postProvider 派生的只读数据
final postCountProvider = Provider<int>((ref) {
return ref.watch(postProvider).posts.length;
});
/// 派生 Provider - 筛选后的帖子列表
final filteredPostsProvider = Provider<List<Post>>((ref) {
final postState = ref.watch(postProvider);
if (postState.selectedPostId == 0) {
return postState.posts;
}
return postState.posts
.where((p) => p.userId == postState.selectedPostId)
.toList();
});
/// 派生 Provider - 帖子是否为空
final postsEmptyProvider = Provider<bool>((ref) {
return ref.watch(postProvider).posts.isEmpty;
});
派生 Provider 的设计遵循了单一职责原则,每个 Provider 只负责一个特定的计算或派生逻辑。使用方无需关心计算过程,只需直接使用计算结果。这种模式借鉴了 Redux 中 selector 的设计思想,有助于保持业务逻辑的内聚性。当计算逻辑发生变化时,只需修改对应的 Provider 定义,使用方无需任何改动。
三、AsyncNotifierProvider 异步数据流实践
3.1 异步状态管理的必要性分析
移动应用中,网络请求是最常见的异步操作来源。用户下拉刷新获取最新数据、上拉加载更多分页数据、搜索功能的实时查询,这些场景都涉及网络请求。与本地状态变更不同,网络请求存在不确定性:请求可能成功返回数据、可能因网络问题超时失败、可能因服务器异常返回错误。传统的处理方式需要在状态类中维护 isLoading、errorMessage 等字段,逻辑分散且容易出错。
考虑一个典型的网络请求场景:用户进入帖子列表页面,应用需要从服务器获取帖子数据。在请求发起时,应显示加载指示器;在请求进行中,应禁用刷新操作防止重复请求;在请求完成时,应根据结果更新界面;在请求失败时,应展示错误信息并提供重试选项。传统的实现方式需要在状态类中维护 isLoading、isRefreshing、errorMessage、lastRequestTime 等多个字段,业务逻辑与状态管理混杂在一起。
Riverpod 提供的 AsyncNotifierProvider 专门针对这种场景进行了优化。通过 AsyncValue 包装机制,可以优雅地处理加载中、成功、错误三种状态的切换。AsyncValue 是 Riverpod 封装的一个特殊类型,它可以处于以下三种状态之一:AsyncLoading(加载中)、AsyncData(成功数据)、AsyncError(错误信息)。
3.2 AsyncNotifierProvider 的实现
以下是 AsyncNotifierProvider 的完整实现示例:
class PostAsyncNotifier extends AsyncNotifier<List<Post>> {
final PostService _postService = PostService();
Future<List<Post>> build() async {
return await _fetchPosts(limit: 20);
}
Future<List<Post>> _fetchPosts({int limit = 20}) async {
final posts = await _postService.getPosts(limit: limit);
return posts.map((p) => p.copyWithImage(
Post.generateImageUrl(p.id),
Post.generateAvatarUrl(p.userId),
)).toList();
}
Future<void> refresh({int limit = 20}) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _fetchPosts(limit: limit));
}
Future<void> loadMore({int limit = 20}) async {
final currentPosts = state.valueOrNull ?? [];
final newPosts = await _fetchPosts(limit: limit);
state = AsyncValue.data([...currentPosts, ...newPosts]);
}
}
final postAsyncProvider = AsyncNotifierProvider<PostAsyncNotifier, List<Post>>(() {
return PostAsyncNotifier();
});
AsyncNotifier 与普通 Notifier 的区别在于状态类型。AsyncNotifier 的状态类型是 Future 而非 T,其中 T 是最终返回的数据类型。build 方法返回 Future,Riverpod 会自动处理异步状态的转换。当 build 方法返回的 Future 处于 pending 状态时,AsyncValue 处于 AsyncLoading 状态;当 Future 成功完成时,AsyncValue 转换为 AsyncData 状态并携带数据;当 Future 抛出异常时,AsyncValue 转换为 AsyncError 状态并携带错误信息。
refresh 方法展示了手动触发刷新的模式。使用 AsyncValue.guard 可以自动捕获异常并转换为 AsyncError 状态,简化了错误处理逻辑。loadMore 方法展示了列表追加数据的模式,通过 valueOrNull 获取当前数据,在其后追加新数据。
3.3 AsyncValue 在 UI 层的使用模式
AsyncValue 提供了丰富的 API 用于状态判断与数据提取。在 Widget 层使用时,需要配合特定的模式来优雅处理各种状态。以下是 PostListView 的实现示例:
class PostListView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final asyncPosts = ref.watch(postAsyncProvider);
return asyncPosts.when(
data: (posts) => _PostListContent(posts: posts),
loading: () => const _LoadingView(),
error: (error, stack) => _ErrorView(
error: error,
onRetry: () => ref.refresh(postAsyncProvider),
),
);
}
}
class _PostListContent extends StatelessWidget {
final List<Post> posts;
const _PostListContent({required this.posts});
Widget build(BuildContext context) {
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) => PostCard(post: posts[index]),
);
}
}
class _LoadingView extends StatelessWidget {
const _LoadingView();
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}
class _ErrorView extends StatelessWidget {
final Object error;
final VoidCallback onRetry;
const _ErrorView({required this.error, required this.onRetry});
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'加载失败',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
error.toString(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('重试'),
),
],
),
);
}
}
when 方法是 AsyncValue 最重要的 API,它根据当前状态自动分发到对应的处理分支。这种模式的优势在于:代码结构清晰、状态覆盖完整、无需手动进行空值判断。在实际开发中,还可以使用 whenData 方法只处理成功状态,loading 和 error 状态可以委托给全局的错误处理机制。
3.4 鸿蒙网络请求场景测试验证
针对 OpenHarmony 平台的特殊性,我们设计了全面的测试用例来验证 AsyncNotifierProvider 在鸿蒙环境中的表现。测试覆盖了以下几个关键场景:
基础网络请求稳定性测试。 在连续多次发起网络请求的场景下,AsyncNotifierProvider 能够正确维护请求状态,未出现状态混乱或内存泄漏问题。每次状态变更都正确触发 UI 重建,加载指示器与内容区域的切换流畅自然。
错误状态正确性测试。 当网络请求失败时(模拟网络断开、服务器超时、返回错误码等场景),AsyncError 状态能够正确携带错误信息,通过 when 方法的 error 分支进行展示。用户可以看到清晰的错误提示,并可以点击重试按钮重新发起请求。
刷新功能完整性测试。 通过 ref.refresh 方法触发的数据重新加载,UI 能够正确显示加载状态遮罩,随后更新数据列表。刷新过程中列表内容保持不变,刷新完成后内容平滑过渡,未出现闪烁或内容跳动问题。
分页加载更多测试。 在支持分页加载的场景中,通过 loadMore 方法追加新数据时,列表能够正确追加项目,未出现列表闪烁、数据丢失或重复添加问题。加载更多完成后,UI 能够正确显示新的数据总量。
状态持久化测试。 在应用切后台再切回前台的场景下,AsyncNotifierProvider 能够正确恢复状态,无需重新加载数据。这种能力对于用户体验的提升非常重要,可以避免用户在切换应用后需要重新等待数据加载。
四、应用入口的 Riverpod 集成
4.1 ProviderScope 的配置方法
将 Riverpod 集成到 Flutter 应用中,需要在应用根节点包裹 ProviderScope 组件。ProviderScope 是 Riverpod 的上下文容器,负责 Provider 的初始化与销毁。以下是 main.dart 的完整改造示例:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/providers.dart';
import 'routing/router.dart';
import 'utils/theme_utils.dart';
final _appRouter = createAppRouter();
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(
const ProviderScope(
child: OpenHarmonyApp(),
),
);
}
class OpenHarmonyApp extends ConsumerWidget {
const OpenHarmonyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(settingsProvider);
final themeMode = settings.flutterThemeMode;
return MaterialApp.router(
title: 'OpenHarmony App',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode,
routerConfig: _appRouter,
);
}
}
ProviderScope 的使用非常简单,只需将整个应用包裹在其中即可。无需传递任何参数,Riverpod 会使用默认配置初始化所有 Provider。如果需要自定义 Provider 的作用域(如在测试环境中使用 mock Provider),可以在 ProviderScope 中通过 providers 参数传入自定义的 Provider 覆盖。
我们将 settingsProvider 的初始化逻辑放在了 SettingsNotifier 的 build 方法中,通过 FutureProvider 控制初始化时机。这种设计确保了设置数据的加载不会阻塞应用启动,提升了用户体验。以下是相关实现:
class SettingsNotifier extends Notifier<SettingsState> {
SettingsState build() {
Future.microtask(() => loadSettings());
return const SettingsState();
}
Future<void> loadSettings() async {
state = state.copyWith(isLoading: true);
await _storageService.init();
state = SettingsState(
themeMode: _storageService.getThemeMode(),
userName: _storageService.getUserName(),
userSignature: _storageService.getUserSignature(),
isDarkMode: _storageService.getThemeMode() == 'dark',
isLoading: false,
);
}
}
使用 Future.microtask 的目的是将初始化延迟到当前事件循环结束之后执行,确保 UI 能够先完成首次渲染。这种模式在需要异步初始化的场景中非常有用,既保证了初始状态的返回,又不会阻塞 UI 渲染。
4.2 ConsumerWidget 的改造模式
在页面层使用 Riverpod,需要将原有的 StatelessWidget 或 StatefulWidget 改造为 ConsumerWidget。以下是 DiscoverPage 的完整改造示例,展示了从 Provider 迁移到 Riverpod 的具体步骤:
class DiscoverPage extends ConsumerStatefulWidget {
const DiscoverPage({super.key});
ConsumerState<DiscoverPage> createState() => _DiscoverPageState();
}
class _DiscoverPageState extends ConsumerState<DiscoverPage> {
final RefreshController _refreshController = RefreshController(
initialRefresh: false,
);
late TabController _tabController;
late AnimationController _bannerController;
void initState() {
super.initState();
_tabController = TabController(length: _tabs.length, vsync: this);
_bannerController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)..repeat();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(postProvider.notifier).loadPosts(limit: 30);
});
}
void dispose() {
_tabController.dispose();
_bannerController.dispose();
_refreshController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final postState = ref.watch(postProvider);
return Scaffold(
appBar: AppBar(
title: const Text('发现'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => ref.read(postProvider.notifier).loadPosts(limit: 30),
),
],
),
body: Column(
children: [
_buildFeaturedBanner(postState),
Expanded(child: _buildPostList(postState)),
],
),
);
}
Widget _buildFeaturedBanner(PostState postState) {
if (postState.posts.isEmpty) {
return const SizedBox.shrink();
}
return Container(
height: 200,
margin: const EdgeInsets.all(16),
child: PageView.builder(
itemCount: postState.posts.take(5).length,
itemBuilder: (context, index) {
return _buildFeaturedCard(postState.posts[index]);
},
),
);
}
Widget _buildPostList(PostState postState) {
if (postState.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (postState.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('错误: ${postState.errorMessage}'),
ElevatedButton(
onPressed: () => ref.read(postProvider.notifier).loadPosts(limit: 30),
child: const Text('重试'),
),
],
),
);
}
return ListView.builder(
itemCount: postState.posts.length,
itemBuilder: (context, index) => PostCard(post: postState.posts[index]),
);
}
}
改造过程中的关键变化需要注意。第一,Widget 类型从 StatefulWidget 改为 ConsumerStatefulWidget,State 类型从 State 改为 ConsumerState。第二,build 方法增加 WidgetRef 参数,通过 ref.watch 监听状态变化,通过 ref.read 调用业务方法。第三,状态对象从 Provider 类变更为独立的 State 类,属性访问方式保持不变(如 postState.posts)。
五、Settings、Todo、Message 模块的迁移实现
5.1 Settings 模块的迁移
Settings 模块负责管理应用的主题模式、用户信息等配置数据。这是一个相对简单的模块,状态字段较少,业务逻辑主要是数据的读取与持久化。以下是 SettingsNotifier 的实现:
class SettingsState {
final String themeMode;
final String userName;
final String userSignature;
final bool isDarkMode;
final bool isLoading;
const SettingsState({
this.themeMode = 'system',
this.userName = 'OpenHarmony 用户',
this.userSignature = '这个人很懒,什么都没写~',
this.isDarkMode = false,
this.isLoading = false,
});
ThemeMode get flutterThemeMode {
switch (themeMode) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
SettingsState copyWith({
String? themeMode,
String? userName,
String? userSignature,
bool? isDarkMode,
bool? isLoading,
}) {
return SettingsState(
themeMode: themeMode ?? this.themeMode,
userName: userName ?? this.userName,
userSignature: userSignature ?? this.userSignature,
isDarkMode: isDarkMode ?? this.isDarkMode,
isLoading: isLoading ?? this.isLoading,
);
}
}
class SettingsNotifier extends Notifier<SettingsState> {
SettingsState build() {
Future.microtask(() => loadSettings());
return const SettingsState();
}
final StorageService _storageService = StorageService();
Future<void> loadSettings() async {
state = state.copyWith(isLoading: true);
await _storageService.init();
state = SettingsState(
themeMode: _storageService.getThemeMode(),
userName: _storageService.getUserName(),
userSignature: _storageService.getUserSignature(),
isDarkMode: _storageService.getThemeMode() == 'dark',
isLoading: false,
);
}
Future<void> setThemeMode(String mode) async {
state = state.copyWith(
themeMode: mode,
isDarkMode: mode == 'dark',
);
await _storageService.setThemeMode(mode);
}
Future<void> setUserName(String name) async {
state = state.copyWith(userName: name);
await _storageService.setUserName(name);
}
Future<void> setUserSignature(String signature) async {
state = state.copyWith(userSignature: signature);
await _storageService.setUserSignature(signature);
}
}
final settingsProvider = NotifierProvider<SettingsNotifier, SettingsState>(() {
return SettingsNotifier();
});
Settings 模块的迁移相对直接,主要的变化是将 ChangeNotifier 模式替换为 Notifier 模式。值得注意的是,我们将主题模式的转换逻辑放在了 State 类中作为 getter 方法,这种设计使得 UI 层可以直接使用 flutterThemeMode 属性,无需关心内部的字符串表示与 Flutter ThemeMode 枚举之间的转换。
5.2 Todo 模块的迁移
Todo 模块负责管理待办事项列表,是四个模块中业务逻辑最复杂的。它涉及数据筛选、用户分组、统计计算等功能。以下是 TodoNotifier 的核心实现:
class TodoState {
final List<TodoItem> todos;
final List<TodoItem> filteredTodos;
final List<TodoItem> allTodos;
final bool isLoading;
final String? errorMessage;
final String currentFilter;
final int selectedUserId;
const TodoState({
this.todos = const [],
this.filteredTodos = const [],
this.allTodos = const [],
this.isLoading = false,
this.errorMessage,
this.currentFilter = 'all',
this.selectedUserId = 0,
});
int get completedCount => allTodos.where((t) => t.completed).length;
int get pendingCount => allTodos.where((t) => !t.completed).length;
int get totalCount => allTodos.length;
Map<String, int> get statistics => {
'total': totalCount,
'completed': completedCount,
'pending': pendingCount,
};
TodoState copyWith({
List<TodoItem>? todos,
List<TodoItem>? filteredTodos,
List<TodoItem>? allTodos,
bool? isLoading,
String? errorMessage,
String? currentFilter,
int? selectedUserId,
}) {
return TodoState(
todos: filteredTodos ?? this.filteredTodos,
filteredTodos: filteredTodos ?? this.filteredTodos,
allTodos: allTodos ?? this.allTodos,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
currentFilter: currentFilter ?? this.currentFilter,
selectedUserId: selectedUserId ?? this.selectedUserId,
);
}
}
class TodoNotifier extends Notifier<TodoState> {
TodoState build() => const TodoState();
final TodoService _todoService = TodoService();
Future<void> loadTodos({int? userId}) async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
List<TodoItem> todos;
int selectedId;
if (userId != null && userId > 0) {
todos = await _todoService.getTodosByUser(userId);
selectedId = userId;
} else {
todos = await _todoService.getTodos();
selectedId = 0;
}
state = state.copyWith(
allTodos: todos,
todos: _applyFilterInternal(todos, state.currentFilter),
isLoading: false,
selectedUserId: selectedId,
);
} catch (e) {
state = state.copyWith(errorMessage: e.toString(), isLoading: false);
}
}
void setFilter(String filter) {
state = state.copyWith(
currentFilter: filter,
todos: _applyFilterInternal(state.allTodos, filter),
);
}
List<TodoItem> _applyFilterInternal(List<TodoItem> todos, String filter) {
switch (filter) {
case 'completed':
return todos.where((t) => t.completed).toList();
case 'pending':
return todos.where((t) => !t.completed).toList();
default:
return List.from(todos);
}
}
}
final todoProvider = NotifierProvider<TodoNotifier, TodoState>(() {
return TodoNotifier();
});
Todo 模块的迁移展示了派生状态的处理模式。filteredTodos 是根据 allTodos 和 currentFilter 计算得出的派生状态,我们将其与原始数据分开存储,简化了 UI 层的访问逻辑。同时,completedCount、pendingCount、statistics 等统计信息也作为派生属性提供,调用方无需关心计算过程。
5.3 Message 模块的迁移
Message 模块负责管理消息通知列表,涉及消息的读取状态、分类筛选、批量操作等功能。以下是 MessageNotifier 的实现:
class MessageState {
final List<Message> messages;
final List<Message> unreadMessages;
final bool isLoading;
final MessageType? currentFilter;
const MessageState({
this.messages = const [],
this.unreadMessages = const [],
this.isLoading = false,
this.currentFilter,
});
int get unreadCount => unreadMessages.length;
MessageState copyWith({
List<Message>? messages,
List<Message>? unreadMessages,
bool? isLoading,
MessageType? currentFilter,
bool clearFilter = false,
}) {
return MessageState(
messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages,
isLoading: isLoading ?? this.isLoading,
currentFilter: clearFilter ? null : (currentFilter ?? this.currentFilter),
);
}
}
class MessageNotifier extends Notifier<MessageState> {
MessageState build() => const MessageState();
final MessageService _messageService = MessageService();
void loadMessages() {
state = state.copyWith(isLoading: true);
final allMessages = _messageService.getAllMessages();
final unreadMessages = _messageService.getUnreadMessages();
List<Message> filteredMessages;
if (state.currentFilter != null) {
filteredMessages = _messageService.getMessagesByType(state.currentFilter!);
} else {
filteredMessages = allMessages;
}
state = MessageState(
messages: filteredMessages,
unreadMessages: unreadMessages,
isLoading: false,
currentFilter: state.currentFilter,
);
}
void setFilter(MessageType? type) {
List<Message> messages;
if (type != null) {
messages = _messageService.getMessagesByType(type);
} else {
messages = _messageService.getAllMessages();
}
state = state.copyWith(
messages: messages,
currentFilter: type,
clearFilter: type == null,
);
}
Future<void> markAsRead(String messageId) async {
await _messageService.markAsRead(messageId);
final unreadMessages = _messageService.getUnreadMessages();
state = state.copyWith(unreadMessages: unreadMessages);
}
Future<void> markAllAsRead() async {
await _messageService.markAllAsRead();
final unreadMessages = _messageService.getUnreadMessages();
loadMessages();
state = state.copyWith(unreadMessages: unreadMessages);
}
Future<void> deleteMessage(String messageId) async {
await _messageService.deleteMessage(messageId);
loadMessages();
}
}
final messageProvider = NotifierProvider<MessageNotifier, MessageState>(() {
return MessageNotifier();
});
Message 模块的迁移展示了 nullable 参数的处理技巧。在 copyWith 方法中,我们添加了 clearFilter 布尔参数来区分"将 filter 设为 null"和"不修改 filter"两种场景,避免了空值传递的歧义。
六、经验总结与最佳实践
6.1 迁移过程的注意事项
在本次 Riverpod 迁移实践中,我们总结出以下关键经验:
第一,状态类的设计要充分考虑不可变性。 每次状态更新都应生成新的状态对象,而非在原对象上修改。这种设计虽然增加了些许代码量,但带来的可追溯性与可测试性提升是值得的。当状态变更出现异常时,不可变设计可以轻松通过打印日志定位问题。此外,不可变状态也天然支持撤销/重做功能的实现。
第二,Notifier 内部的逻辑要保持精简。 将复杂的计算逻辑分散到派生 Provider 中,可以保持 Notifier 的清晰结构。Notifier 的核心职责应当是状态管理与业务方法调用,而非状态计算。派生 Provider 的计算结果会被缓存,只有依赖的原始状态变更时才会重新计算,这种机制确保了性能不会因为派生计算而下降。
第三,异步操作优先使用 AsyncNotifierProvider。 对于涉及网络请求的业务模块,使用 AsyncValue 处理异步状态可以大幅简化代码逻辑,避免手动维护 isLoading、errorMessage 等字段。AsyncValue.when 方法提供了优雅的状态分发机制,代码结构清晰且不易出错。AsyncValue 还支持 andWhen、or 等组合操作,可以应对更复杂的异步场景。
第四,渐进式迁移要控制节奏。 不要试图一次性完成所有模块的迁移,这样风险太大且难以调试。按照业务优先级逐个模块迁移,每完成一个模块都进行充分测试,可以有效控制风险。迁移过程中保持新旧代码并行,即使出现问题也可以快速回滚。建议每个模块迁移完成后进行一次完整的回归测试。
第五,合理使用派生 Provider。 派生 Provider 可以将复杂的计算逻辑封装起来,使用方无需关心计算过程。需要注意,派生 Provider 会自动缓存计算结果,直到其依赖的状态变更才会重新计算。对于计算量较大的派生逻辑,应评估是否需要手动添加缓存策略。
6.2 OpenHarmony 平台兼容性验证结论
经过实际项目验证,Riverpod 在 OpenHarmony Flutter 引擎上的表现与 Android 平台基本一致。核心功能包括状态监听、异步处理、Provider 派生等均工作正常,未发现任何平台相关的兼容性问题。AsyncNotifierProvider 在鸿蒙网络请求场景中的表现尤为稳定,能够正确处理请求的发起、响应、错误等各个阶段。
本次适配工作验证了 Riverpod 作为 Flutter 跨平台状态管理方案的可行性。开发者可以放心地在 OpenHarmony 项目中使用 Riverpod,无需担心平台兼容性问题。同时,Riverpod 的纯 Dart 实现特性意味着它可以随着 Flutter 对 OpenHarmony 支持的完善而自动受益。
6.3 代码托管与社区协作
本次适配工作涉及的完整代码已托管至 AtomGit 平台,仓库地址为 https://atomgit.com/openharmony-flutter/riverpod-adaptation 。代码按照业务模块组织,每个 Provider 的新旧实现并行存放,便于开发者对比学习。仓库同时包含了完整的示例项目,可以直接导入开发环境运行验证。
我们鼓励开发者在使用过程中提出问题与建议,通过社区协作不断完善这一适配方案。OpenHarmony 的生态建设需要每一位开发者的参与和贡献,状态管理作为应用架构的核心组成部分,值得投入更多精力去探索与优化。
这是我的运行截图:
结语
Riverpod 作为 Flutter 生态中功能最为强大的状态管理方案之一,其在 OpenHarmony 平台上的稳定表现为跨平台应用开发提供了更多可能性。通过本次适配实践,我们验证了 Riverpod 核心功能的鸿蒙兼容性,探索了渐进式迁移的可行路径,并积累了 AsyncNotifierProvider 在真实业务场景中的使用经验。
状态管理的演进永无止境,Riverpod 也不是终点。随着应用复杂度的提升,开发者需要持续关注状态管理的最佳实践,根据项目实际情况选择最适合的方案。OpenHarmony 作为一个充满活力的新生平台,其生态建设需要每一位开发者的参与和贡献。
欢迎加入开源鸿蒙跨平台社区,与志同道合的开发者共同探索跨平台技术的无限可能。在社区中,你可以分享自己的适配经验,也可以从他人的实践中学习。跨平台技术的未来,需要我们共同努力书写。
更多推荐

所有评论(0)