Flutter 三方库 Riverpod 的鸿蒙化适配指南
userId,int?id,String?title,bool?id: id??this.id,??看看上面这个TodoItem类,我们定义了完整的copyWith方法,这是实现不可变状态更新的关键所在~每次更新数据时,我们不是修改原对象,而是创建一个全新的对象,这样做的好处多多的呢!现在最激动人心的时刻到了——让我们看看如何使用Riverpod定义各种Provider!
Flutter 三方库 Riverpod 的鸿蒙化适配指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
嗨各位小伙伴们大家好呀!今天要和大家分享一个超级实用的Flutter状态管理方案——Riverpod在鸿蒙设备上的适配之旅✨
话说Flutter发展到现在啊,状态管理可以说是一个永恒的话题。从最早的Provider一路走来,社区里诞生了各种各样优秀的状态管理方案。而Riverpod作为后起之秀,凭借其独特的设计理念和强大的功能,已经成为了Flutter生态中最受欢迎的状态管理库之一。那么问题来了——在鸿蒙化的大潮中,Riverpod能否稳定运行呢?答案是肯定的!接下来就让我带大家一起探索Riverpod的鸿蒙化适配之旅吧~
一、为什么选择Riverpod?
咳咳,在开始动手之前,我们先来聊聊为什么选择Riverpod作为我们的新状态管理方案吧!
首先呢,Riverpod具有以下几个让人心动不已的优点:
第一,依赖注入超级方便! Riverpod采用了声明式的Provider定义方式,让我们可以轻松实现依赖注入和服务定位,再也不用担心循环依赖的噩梦啦~
第二,编译时安全性! 不同于Provider的运行时错误,Riverpod的很多错误都能在编译时被捕获,这意味着更少的线上bug和更安心的开发体验呢~
第三,测试友好! Riverpod的设计天然支持mock,我们可以轻松地为每个Provider编写单元测试,妈妈再也不用担心我的代码无法测试啦!
第四,异步数据流处理强大! 内置的AsyncNotifierProvider和StreamProvider让处理异步数据变得轻而易举,特别适合网络请求这种场景呢~
二、核心适配方案设计
好啦,理论部分讲完了,现在让我们开始动手实践吧!
2.1 状态类设计
首先呢,我们要聊一聊状态类的设计。Riverpod推荐使用不可变(Immutable)状态类,这可不是什么花里胡哨的概念哦——它的核心思想是每次状态更新都生成一个新的状态对象,而不是在原对象上修改。这种设计虽然看起来代码量多了一点点,但是带来的可追溯性和可测试性提升是非常值得的!
想象一下,当状态变更出现异常时,不可变设计可以让我们轻松通过打印日志定位问题。而且呀,这种设计天然支持撤销/重做功能的实现,简直是一举多得呢~
2.2 数据模型定义
class TodoItem {
final int userId;
final int id;
final String title;
final bool completed;
TodoItem({
required this.userId,
required this.id,
required this.title,
required this.completed,
});
factory TodoItem.fromJson(Map<String, dynamic> json) {
return TodoItem(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
completed: json['completed'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'userId': userId,
'id': id,
'title': title,
'completed': completed,
};
}
TodoItem copyWith({
int? userId,
int? id,
String? title,
bool? completed,
}) {
return TodoItem(
userId: userId ?? this.userId,
id: id ?? this.id,
title: title ?? this.title,
completed: completed ?? this.completed,
);
}
}
看看上面这个TodoItem类,我们定义了完整的copyWith方法,这是实现不可变状态更新的关键所在~每次更新数据时,我们不是修改原对象,而是创建一个全新的对象,这样做的好处多多的呢!
2.3 服务层封装
接下来让我们看看网络请求服务层的设计。这里我们使用Dio作为HTTP客户端,并结合Riverpod进行状态管理。
class TodoService {
static const String _baseUrl = 'https://jsonplaceholder.typicode.com';
final Dio _dio;
TodoService(this._dio) {
_dio.options = BaseOptions(
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
}
Future<List<TodoItem>> getTodos() async {
try {
final response = await _dio.get('$_baseUrl/todos');
final List<dynamic> data = response.data;
return data.map((json) => TodoItem.fromJson(json)).toList();
} on DioException catch (e) {
throw _handleError(e);
}
}
Future<TodoItem> getTodoById(int id) async {
try {
final response = await _dio.get('$_baseUrl/todos/$id');
return TodoItem.fromJson(response.data);
} on DioException catch (e) {
throw _handleError(e);
}
}
Exception _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return Exception('连接超时,请检查网络设置~');
case DioExceptionType.sendTimeout:
return Exception('发送请求超时啦~');
case DioExceptionType.receiveTimeout:
return Exception('接收数据超时了呢~');
case DioExceptionType.badResponse:
return Exception('服务器开小差了: ${e.response?.statusCode}');
case DioExceptionType.cancel:
return Exception('请求被取消了哦~');
case DioExceptionType.connectionError:
return Exception('连接错误,请检查网络~');
default:
return Exception('发生了一些小意外: ${e.message}');
}
}
}
2.4 Provider定义
现在最激动人心的时刻到了——让我们看看如何使用Riverpod定义各种Provider!
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import '../models/todo_item.dart';
import '../services/todo_service.dart';
// Dio实例Provider
final dioProvider = Provider<Dio>((ref) {
return Dio();
});
// TodoService Provider
final todoServiceProvider = Provider<TodoService>((ref) {
final dio = ref.watch(dioProvider);
return TodoService(dio);
});
// Todo列表状态
class TodoListState {
final List<TodoItem> todos;
final bool isLoading;
final String? errorMessage;
const TodoListState({
this.todos = const [],
this.isLoading = false,
this.errorMessage,
});
TodoListState copyWith({
List<TodoItem>? todos,
bool? isLoading,
String? errorMessage,
}) {
return TodoListState(
todos: todos ?? this.todos,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
);
}
}
// Todo列表Notifier
class TodoListNotifier extends Notifier<TodoListState> {
TodoListState build() {
return const TodoListState();
}
Future<void> loadTodos() async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
final todoService = ref.read(todoServiceProvider);
final todos = await todoService.getTodos();
state = state.copyWith(todos: todos, isLoading: false);
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
);
}
}
Future<void> refresh() async {
await loadTodos();
}
}
// 核心Provider定义
final todoListProvider = NotifierProvider<TodoListNotifier, TodoListState>(
TodoListNotifier.new,
);
这一段代码可是整个方案的核心所在呢!让我们来仔细解读一下~
首先是Dio实例的Provider——这个Provider负责管理Dio单例,我们可以在需要的地方通过ref.watch来获取它。然后是TodoService的Provider,它依赖于Dio Provider,实现了很好的解耦。
最重要的是TodoListNotifier这个类啦!它继承自Notifier,这是Riverpod中处理同步状态的利器。我们通过build方法返回初始状态,然后通过暴露的方法来更新状态。每个方法最后都会调用state = xxx来触发UI更新,这种写法既清晰又易于追踪状态变化~
2.5 异步数据流处理
对于需要处理异步加载的场景,Riverpod还提供了AsyncNotifier这个大招!让我们一起来看看吧~
// 异步Todo详情Provider
class TodoDetailNotifier extends AsyncNotifier<TodoItem?> {
Future<TodoItem?> build() async {
return null;
}
Future<void> loadTodo(int id) async {
state = const AsyncValue.loading();
try {
final todoService = ref.read(todoServiceProvider);
final todo = await todoService.getTodoById(id);
state = AsyncValue.data(todo);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
void clear() {
state = const AsyncValue.data(null);
}
}
final todoDetailProvider =
AsyncNotifierProvider<TodoDetailNotifier, TodoItem?>(
TodoDetailNotifier.new,
);
AsyncNotifier特别适合处理网络请求这种异步操作。它内置了AsyncValue状态,包含了loading、data和error三种状态,让我们可以优雅地处理各种加载场景。
三、渐进式迁移策略
看到这里,小伙伴们可能会问了:我们的项目已经大量使用了Provider,能不能平滑过渡到Riverpod呢?当然可以啦!这里给大家介绍几种实用的渐进式迁移策略~
策略一:平行运行法
在新功能中使用Riverpod,旧功能保持Provider不变。两套系统同时运行,互不干扰。等新功能稳定后,再逐步迁移旧代码。这种方式适合项目周期比较宽裕的情况~
策略二:按模块迁移法
将项目按照功能模块划分,每次只迁移一个模块。比如先迁移用户认证模块,再迁移商品模块,最后迁移订单模块。迁移完成后立即测试,确保功能正常后再进行下一个模块。这种方式适合中大型项目,风险可控~
策略三:核心抽离法
先把共用的数据模型和服务层抽离出来,用Riverpod重写这一部分。UI层暂时保持Provider不变。等核心层稳定后,再逐步将UI层也迁移到Riverpod。这种方式可以让迁移工作更加聚焦~
四、UI层集成实践
好啦,核心代码讲完了,现在让我们看看如何在UI层使用这些Provider吧!
class TodoListPage extends ConsumerWidget {
const TodoListPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final todoState = ref.watch(todoListProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('我的待办清单'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.read(todoListProvider.notifier).refresh();
},
),
],
),
body: _buildBody(context, todoState, ref),
);
}
Widget _buildBody(BuildContext context, TodoListState state, WidgetRef ref) {
if (state.isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('正在加载数据中...'),
],
),
);
}
if (state.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'哎呀出错啦: ${state.errorMessage}',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
ref.read(todoListProvider.notifier).loadTodos();
},
child: const Text('重新加载'),
),
],
),
);
}
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) {
final todo = state.todos[index];
return ListTile(
leading: Icon(
todo.completed ? Icons.check_circle : Icons.circle_outlined,
color: todo.completed ? Colors.green : Colors.grey,
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
),
),
subtitle: Text('用户ID: ${todo.userId}'),
);
},
);
}
}
注意到这里使用的是ConsumerWidget而不是普通的StatelessWidget,这让我们可以通过ref来访问状态。ref.watch用于监听状态变化并重建UI,ref.read用于读取状态而不建立监听关系。
五、鸿蒙设备运行验证

(上图展示了Riverpod状态管理在鸿蒙设备上的运行效果,可以看到Todo列表数据成功加载,异步状态管理运作良好~)
经过实际测试验证,Riverpod在鸿蒙设备上运行稳定,各项功能表现正常:
- Provider的依赖注入机制工作正常
- Notifier的状态更新可以正确触发UI重建
- AsyncNotifier的异步数据流处理表现优异
- 网络请求与状态管理配合默契
特别值得一提的是AsyncNotifierProvider在处理网络请求时展现了出色的稳定性。即使在网络不佳的情况下,也能优雅地展示加载状态和错误信息,不会出现崩溃或白屏问题~
六、总结与展望
好啦,今天的分享就到这里啦!让我们来总结一下今天学到的知识吧~
核心要点回顾:
第一,Riverpod的不可变状态设计虽然看似增加了代码量,但带来了可追溯性和可测试性的显著提升,这是非常值得的投资呢~
第二,通过Notifier和AsyncNotifier,我们可以优雅地处理同步和异步两种状态管理场景,代码结构清晰易懂~
第三,渐进式迁移策略让我们可以在不影响现有功能的前提下,逐步完成从Provider到Riverpod的过渡~
展望未来:
Riverpod在鸿蒙设备上的稳定表现,为Flutter鸿蒙化开发提供了可靠的状态管理保障。随着鸿蒙生态的持续发展,相信会有更多优秀的Flutter三方库完成适配,让我们的开发工作越来越便捷~
最后呢,欢迎大家加入开源鸿蒙跨平台社区,一起探讨Flutter鸿蒙化的技术奥秘!如果有任何问题,欢迎在评论区留言交流哦~
那么,我们下期再见啦!拜拜~👋
更多推荐

所有评论(0)