导言:
在上一讲中,我们学习了 Provider,它是一个非常优秀且易于上手的状态管理方案。然而,随着业务逻辑变得复杂,你可能会发现一些问题:业务逻辑分散在各个地方,难以进行单元测试,状态变更的流程不够清晰。
本讲将带你走向更“声明式”、更“可预测”的状态管理。我们将探索两种现代且强大的模式:Bloc 和 Riverpod。它们能强制地将你的业务逻辑与UI界面分离,让代码更健壮、更易于维护和测试。
一、 为什么需要进阶状态管理?
我们先回顾一下简单状态管理可能遇到的挑战:
-
业务逻辑与UI耦合:在
ChangeNotifier的方法里直接调用网络请求,UI层既负责显示又负责处理逻辑。 -
可测试性差:因为逻辑和UI耦合,测试业务逻辑需要构建Widget,非常笨重。
-
状态变更难以追踪:当应用复杂时,一个状态的变化可能由多个事件触发,很难追踪是哪个事件导致了状态的改变。
-
“面条式”代码:所有逻辑都写在同一个类里,随着功能增加,类会变得臃肿不堪。
Bloc 和 Riverpod 的核心思想就是:让状态的变化变得像“状态机”一样清晰可预测。
UI层 => 触发事件(Event) => 业务逻辑处理 => 产生新状态(State) => UI层根据新状态刷新
二、 Bloc 模式:严格的单向数据流
Bloc 模式将应用分为三个核心部分:
-
Events(事件):UI层触发的动作,如
LoginButtonPressed。 -
Bloc(业务逻辑组件):接收Events,处理业务逻辑,并输出States。
-
States(状态):应用在某一时刻的表现状态,如
LoginInitial,LoginLoading,LoginSuccess,LoginFailure。
实战:用 Bloc 实现登录流程
我们将使用最流行的 flutter_bloc 库。
1. 定义状态(State)
首先,我们定义登录过程中所有可能的状态。
dart
复制
下载
// login_state.dart
part of 'login_bloc.dart';
// 使用 sealed class (推荐) 或 abstract class 来定义所有可能的状态
// 这确保了状态类型的完备性,非常强大!
sealed class LoginState extends Equatable {
const LoginState();
@override
List<Object> get props => [];
}
// 初始状态
class LoginInitial extends LoginState {}
// 正在登录中
class LoginLoading extends LoginState {}
// 登录成功
class LoginSuccess extends LoginState {
final String userEmail;
const LoginSuccess(this.userEmail);
}
// 登录失败
class LoginFailure extends LoginState {
final String errorMessage;
const LoginFailure(this.errorMessage);
}
2. 定义事件(Event)
然后,定义能触发状态变化的事件。
dart
复制
下载
// login_event.dart
part of 'login_bloc.dart';
// 同样使用 sealed class 或 abstract class
sealed class LoginEvent extends Equatable {
const LoginEvent();
@override
List<Object> get props => [];
}
// 当用户点击登录按钮时触发的事件
class LoginButtonPressed extends LoginEvent {
final String email;
final String password;
const LoginButtonPressed({
required this.email,
required this.password,
});
@override
List<Object> get props => [email, password];
}
3. 创建 Bloc 类
这是核心,它包含了处理事件的业务逻辑。
dart
复制
下载
// login_bloc.dart
import 'package:bloc/bloc.dart';
class LoginBloc extends Bloc<LoginEvent, LoginState> {
// 可以在这里注入你的认证Repository
final AuthRepository authRepository;
LoginBloc({required this.authRepository}) : super(LoginInitial()) {
// 注册事件处理器:当收到 `LoginButtonPressed` 事件时,执行 `_onLoginButtonPressed` 方法。
on<LoginButtonPressed>(_onLoginButtonPressed);
}
Future<void> _onLoginButtonPressed(
LoginButtonPressed event,
Emitter<LoginState> emit,
) async {
// 1. 发出“加载中”状态
emit(LoginLoading());
try {
// 2. 调用认证接口(业务逻辑)
final user = await authRepository.authenticate(
email: event.email,
password: event.password,
);
// 3. 如果成功,发出“成功”状态
emit(LoginSuccess(user.email));
} catch (error) {
// 4. 如果失败,发出“失败”状态
emit(LoginFailure(error.toString()));
}
}
}
4. 在UI层中使用
dart
复制
下载
// login_screen.dart
import 'package:flutter_bloc/flutter_bloc.dart';
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider(
// 提供LoginBloc给子树
create: (context) => LoginBloc(authRepository: AuthRepository()),
child: LoginForm(),
),
);
}
}
class LoginForm extends StatelessWidget {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
// 使用 BlocBuilder 来响应状态变化并重建UI
return BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
// 根据不同的状态,显示不同的UI
if (state is LoginLoading) {
return const Center(child: CircularProgressIndicator());
}
return Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(controller: _emailController, decoration: InputDecoration(labelText: 'Email')),
TextField(controller: _passwordController, obscureText: true, decoration: InputDecoration(labelText: 'Password')),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (state is! LoginLoading) { // 防止重复提交
// 触发事件!这是UI层唯一与Bloc交互的方式
context.read<LoginBloc>().add(LoginButtonPressed(
email: _emailController.text,
password: _passwordController.text,
));
}
},
child: Text('Login'),
),
// 处理错误状态
if (state is LoginFailure)
Text('Error: ${state.errorMessage}', style: TextStyle(color: Colors.red)),
// 处理成功状态
if (state is LoginSuccess)
Text('Welcome, ${state.userEmail}!', style: TextStyle(color: Colors.green)),
],
),
);
},
);
}
}
Bloc 优势总结:
-
清晰可预测:状态变化路径一目了然。
-
易于测试:你可以单独测试Bloc,只需输入Event,验证输出的State。
-
强大的DevTools:有专门的Bloc DevTools用于调试和追踪状态变化。
三、 Riverpod:下一代的状态管理与依赖注入
Riverpod 被设计为 Provider 的改进版,解决了 Provider 的诸多痛点(如对BuildContext的依赖、编译时安全等)。它同时也是一个强大的依赖注入框架。
实战:用 Riverpod 实现相同的登录流程
我们将使用 flutter_riverpod 库。
1. 创建状态Notifier
Riverpod 推荐使用 AsyncNotifier 来处理异步操作。
dart
复制
下载
// login_notifier.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 状态类
class LoginState {
final bool isLoading;
final String? userEmail;
final String? errorMessage;
const LoginState({
this.isLoading = false,
this.userEmail,
this.errorMessage,
});
}
// Notifier 类
class LoginNotifier extends AsyncNotifier<LoginState> {
// 初始化状态
@override
LoginState build() {
return const LoginState();
}
// 提供一个获取 AuthRepository 的方法(依赖注入)
AuthRepository get _authRepository => ref.read(authRepositoryProvider);
// 登录方法
Future<void> login(String email, String password) async {
// 更新状态为加载中
state = const AsyncValue.data(LoginState(isLoading: true));
// 使用 AsyncValue.guard 优雅地处理异步操作和错误
state = await AsyncValue.guard(() async {
final user = await _authRepository.authenticate(email: email, password: password);
return LoginState(userEmail: user.email); // 成功,返回新状态
});
// 如果出错,state 会自动包含错误信息
}
}
// 提供 Notifier 的全局 Provider
final loginNotifierProvider = AsyncNotifierProvider<LoginNotifier, LoginState>(() {
return LoginNotifier();
});
2. 在UI层中使用
dart
复制
下载
// login_screen.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LoginScreen extends ConsumerWidget { // 使用 ConsumerWidget 替代 StatelessWidget
@override
Widget build(BuildContext context, WidgetRef ref) { // 多了一个 WidgetRef ref 参数
// 监听 loginNotifierProvider 的状态
final loginState = ref.watch(loginNotifierProvider);
return Scaffold(
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(/* ... */),
TextField(/* ... */),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 调用 Notifier 中的方法
ref.read(loginNotifierProvider.notifier).login('user@example.com', 'password');
},
child: Text('Login'),
),
// 根据状态显示UI
if (loginState.isLoading) const CircularProgressIndicator(),
if (loginState.hasError) // 处理错误
Text('Error: ${loginState.error}', style: TextStyle(color: Colors.red)),
if (loginState.value?.userEmail != null) // 处理成功
Text('Welcome, ${loginState.value!.userEmail}!', style: TextStyle(color: Colors.green)),
],
),
),
);
}
}
Riverpod 优势总结:
-
编译时安全:错误的Provider引用会在编译时报错,而非运行时。
-
不依赖BuildContext:可以在任何地方(包括类内部)访问Provider。
-
强大的依赖注入:轻松管理、覆盖和测试依赖项。
-
出色的组合性:Provider之间可以轻松地相互引用。
-
原生支持异步操作:
AsyncNotifier和AsyncValue让异步状态处理变得异常简单和安全。
四、 如何选择?Bloc vs Riverpod
这是一个社区常见问题,没有绝对答案。
-
选择 Bloc,如果你:
-
喜欢非常严格的架构和单向数据流。
-
项目非常复杂,需要清晰地追踪每一个状态变化的来源。
-
团队需要统一的、强制性的开发模式。
-
看重强大的调试工具(Bloc DevTools)。
-
-
选择 Riverpod,如果你:
-
希望一个更灵活、更现代的解决方案。
-
看中编译时安全和卓越的开发者体验。
-
需要强大的依赖注入功能。
-
觉得Bloc的模板代码(Boilerplate)太多,希望更简洁。
-
它与
flutter_hooks结合得非常好。
-
结论: 两者都是顶级的选择。对于大多数新项目,Riverpod 因其灵活性和开发效率正变得越来越流行。但对于超大型或需要严格规范的团队,Bloc 依然是无懈可击的选择。
1588

被折叠的 条评论
为什么被折叠?



