Riverpod集中式与分散式的两种思路
前言
要说起 Riverpod 大家肯定不陌生,毕竟也是目前官方推荐的,它的优势或者说特点是什么呢?
不同的人有不同的说法,强大的状态管理,声明式的编程方法,复杂UI的简化,增强工具的支持等等。
但是抛开表看看本质,我们为什么要用Riverpod,是因为它的灵活性,也就是可重用可组合性。
易于可重复使用,高度可组合,易维护。
所以天然上来说我们既然用了 Riverpod 就得是分散式组合式的使用方式,但是实际开发上真的这样就万事大吉了吗?
你这么说的我都不自信了,那么一个页面的功能最佳实践是怎样的呢? 还是举例吧。比如一个 UserProfilePage 页面,我需要展示用户的信息,全部行业列表,分类列表这些信息。
方案一,也就是分散的做法,通过三种不同的ProfileState IndustryState CategoryState 分别对应三种不同的 Provider ,不需要一个 ViewModel/Controller(叫法不同后面统一用ViewModel替代)去统一管理,异步可以选择 AsyncProvider 同步的选择 Provider,直接在页面中通过 watch 去观察三种状态去更新页面。
方案二,集中式管理,我创建一个 UserPorfileState 保存页面的全部状态,再加上一个 UserProfileViewModel 继承自 Notifier,通过 Provider 提供给 UserProfilePage 统一使用。三种数据的获取都写在 UserProfileViewModel 中,通过 UserPorfileState 内部统一定义三种数据,然后在页面中通过 watch 去观察并更新页面。
其实也不是什么稀奇的事情,一种前端开发者习惯写法,一种是移动端开发者习惯写法。都能完成对应的功能。两种方案都有其优点和适用场景。具体选择哪种方案取决于你的具体需求、项目规模和代码维护性。以下是对这两种方案的详细分析。
一、集中式处理
优点:
- 统一管理: 所有数据和业务逻辑集中在一个 ViewModel 中,便于管理。
- 状态一致性: 一个统一的状态类可以保证页面各部分数据的一致性。
- 简单的状态管理: 页面只需关注一个 Provider,简化了状态的订阅和管理。
缺点:
- 复杂性: 如果页面的数据和逻辑非常复杂,ViewModel 可能会变得非常庞大和难以维护。
- 耦合性高: 页面和 ViewModel 的耦合度较高,影响部分功能的独立性和可测试性。
我们以上面的场景为例:
final userProfileProvider = StateNotifierProvider<UserProfileViewModel, UserProfileState>((ref) {
return UserProfileViewModel();
});
class UserProfileState {
final User? user;
final List<Industry>? industries;
final List<Category>? categories;
UserProfileState({this.user, this.industries, this.categories});
}
class UserProfileViewModel extends StateNotifier<UserProfileState> {
UserProfileViewModel() : super(UserProfileState());
Future<void> fetchUserProfile() async {
// 获取用户信息
final user = await fetchUser();
// 获取行业列表
final industries = await fetchIndustries();
// 获取分类列表
final categories = await fetchCategories();
state = UserProfileState(
user: user,
industries: industries,
categories: categories,
);
}
}
class UserProfilePage extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final state = watch(userProfileProvider);
if (state.user == null || state.industries == null || state.categories == null) {
return CircularProgressIndicator();
}
return Scaffold(
// 页面布局逻辑
);
}
}
一个页面一个控制器,在内部统一的处理数据加载与 UI Loading 的逻辑处理,很符合移动端开发的习惯,如果是移动端的开发者很容易就上手。
但是当一些重复的场景每次都要写重复的逻辑,比如支付的发起与校验,类似这种功能我们其实是更方便使用(Service/UserCase叫法不同)单独的服务或用例来承载。
二、分散式处理
优点:
- 解耦: 不同的数据源通过不同的 Provider 管理,降低了耦合度。
- 独立性: 各个 Provider 独立存在,便于单独测试和复用。
- 灵活性: 可以根据需要灵活组合各个 Provider。
缺点:
- 复杂的状态管理: 页面需要同时关注多个 Provider,增加了状态管理的复杂性。
- 潜在的性能问题: 如果多个 Provider 状态变化频繁,可能会导致页面频繁重建。
final userProvider = FutureProvider<User>((ref) async {
return fetchUser();
});
final industriesProvider = FutureProvider<List<Industry>>((ref) async {
return fetchIndustries();
});
final categoriesProvider = FutureProvider<List<Category>>((ref) async {
return fetchCategories();
});
class UserProfilePage extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final userAsyncValue = watch(userProvider);
final industriesAsyncValue = watch(industriesProvider);
final categoriesAsyncValue = watch(categoriesProvider);
return userAsyncValue.when(
data: (user) => industriesAsyncValue.when(
data: (industries) => categoriesAsyncValue.when(
data: (categories) {
return Scaffold(
// 页面布局逻辑
);
},
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
}
}
这里的状态加 when 的用法是 future 的加载状态处理,也可以用更推荐的 maybeWhen:
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
backgroundColor: context.isSmallScreen ? null : Colors.transparent,
centerTitle: true,
title: const Text('Favorites'),
),
body: ref.watch(favoritesNotifierProvider).maybeWhen(
loading: () => const CircularProgressIndicator(),
orElse: () => const SizedBox.shrink(),
data: (favorites) {
if (favorites.isEmpty) const EmptyView();
return Wrap(
children: [
...favorites.map((entry) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5.0,
vertical: 5.0,
),
child: BookItem(
img: entry.link![1].href!,
title: entry.title!.t!,
entry: entry,
),
);
}),
],
);
},
),
);
}
如果全部使用分散式的我们的加载状态无法统一管理,无法处理并发的任务和顺序和 Loading 加载的状态。除非我们再用一个控制器去管理这三个 Provider,那么本质上就和集中式的处理没什么区别了。
三、综合使用
方案一 适合数据和逻辑相对简单的场景,便于集中管理和状态一致性。
方案二 适合想要降低耦合度、提高模块化和复用性的场景,但需要更复杂的状态管理。
综合使用这两种方案能够结合它们的优点,既保持灵活性,又能集中管理。你可以在一个 ViewModel 中调用多个 Provider,并将它们的结果综合到一个统一的状态类中。这样,你可以在一个地方管理页面的整体状态,同时保持各个数据源的独立。
- 定义多个独立的 Provider 分别管理不同的数据源。
- 定义一个 ViewModel 类,集中管理页面的状态。
- 在 ViewModel 中调用这些独立的 Provider,将结果赋值给一个统一的状态类。
- 使用 StateNotifierProvider 将 ViewModel 提供给页面。
- 在页面中观察和使用统一的状态类。
定义数据源 Provider
final userProvider = FutureProvider<User>((ref) async {
return fetchUser();
});
final industriesProvider = FutureProvider<List<Industry>>((ref) async {
return fetchIndustries();
});
final categoriesProvider = FutureProvider<List<Category>>((ref) async {
return fetchCategories();
});
定义统一的状态类
class UserProfileState {
final User? user;
final List<Industry>? industries;
final List<Category>? categories;
UserProfileState({this.user, this.industries, this.categories});
UserProfileState copyWith({
User? user,
List<Industry>? industries,
List<Category>? categories,
}) {
return UserProfileState(
user: user ?? this.user,
industries: industries ?? this.industries,
categories: categories ?? this.categories,
);
}
}
定义 ViewModel 控制类
class UserProfileViewModel extends StateNotifier<UserProfileState> {
UserProfileViewModel(ProviderReference ref)
: super(UserProfileState()) {
_initialize(ref);
}
void _initialize(ProviderReference ref) {
ref.listen(userProvider, (userAsyncValue) {
userAsyncValue.when(
data: (user) {
state = state.copyWith(user: user);
},
loading: () {},
error: (err, stack) {
// 处理错误
},
);
});
ref.listen(industriesProvider, (industriesAsyncValue) {
industriesAsyncValue.when(
data: (industries) {
state = state.copyWith(industries: industries);
},
loading: () {},
error: (err, stack) {
// 处理错误
},
);
});
ref.listen(categoriesProvider, (categoriesAsyncValue) {
categoriesAsyncValue.when(
data: (categories) {
state = state.copyWith(categories: categories);
},
loading: () {},
error: (err, stack) {
// 处理错误
},
);
});
}
}
提供 ViewModel
final userProfileViewModelProvider = StateNotifierProvider<UserProfileViewModel, UserProfileState>((ref) {
return UserProfileViewModel(ref);
});
页面中使用 ViewModel 和统一状态类
class UserProfilePage extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final state = watch(userProfileViewModelProvider);
if (state.user == null || state.industries == null || state.categories == null) {
return CircularProgressIndicator();
}
return Scaffold(
// 页面布局逻辑
);
}
}
这种综合方案结合了集中管理和灵活性的优点,通过在 ViewModel 中调用多个 Provider,并将它们的结果统一到一个状态类中进行管理。这样,你可以在页面中通过一个统一的状态类来观察和更新页面,同时保留各个数据源的独立性和模块化。
当然对于一些通用单独的逻辑我们还是应该使用单独的 Provider 来替代,比如上面我们提到的支付与校验的逻辑,我们就能使用一个 PaymentUserCase / PaymentService 来单独处理。
总的来说思路就是保证每个 Provider 的单一职责,而 ViewModel 作为控制器则整合各个 Provider 对页面状态负责。
这样对于 缓存机制、懒加载、并行请求、错误处理、重用机制都有很好的帮助。
总结
在这篇文章中,我们回顾到 Provider 的移动端集中式的用法和前端分散式的用法,以及他们的优缺点。以及单独 Provider 的状态Loading管理用法。
同时我们探讨了如何结合使用多个 Provider 和 ViewModel 来管理复杂应用中的状态。这种综合方案能够有效结合集中管理与模块化的优点,在保持灵活性的同时,确保数据和逻辑的独立性。
通过定义独立的数据源 Provider 并在 ViewModel 中进行集中管理,我们可以在一个地方维护页面的整体状态。这种设计不仅提高了代码的可维护性,还增强了各个模块的重用性和可测试性。
在优化部分,我们讨论了如何通过缓存机制、懒加载、并行请求等方法来提升应用性能。此外,采用单一责任原则和封装业务逻辑有助于减少代码耦合,提高模块化程度。
在实际应用中,我们可以灵活选择使用各种 Provider /StreamProvider / FutureProvider 来使用,更灵活的实现框架搭建。
OK,那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我真的很重要。
这一期就此完结了。