第13讲:Bloc/Riverpod进阶 - 构建可预测、易于测试的业务逻辑

导言:

在上一讲中,我们学习了 Provider,它是一个非常优秀且易于上手的状态管理方案。然而,随着业务逻辑变得复杂,你可能会发现一些问题:业务逻辑分散在各个地方,难以进行单元测试,状态变更的流程不够清晰。

本讲将带你走向更“声明式”、更“可预测”的状态管理。我们将探索两种现代且强大的模式:Bloc 和 Riverpod。它们能强制地将你的业务逻辑UI界面分离,让代码更健壮、更易于维护和测试。


一、 为什么需要进阶状态管理?

我们先回顾一下简单状态管理可能遇到的挑战:

  1. 业务逻辑与UI耦合:在 ChangeNotifier 的方法里直接调用网络请求,UI层既负责显示又负责处理逻辑。

  2. 可测试性差:因为逻辑和UI耦合,测试业务逻辑需要构建Widget,非常笨重。

  3. 状态变更难以追踪:当应用复杂时,一个状态的变化可能由多个事件触发,很难追踪是哪个事件导致了状态的改变。

  4. “面条式”代码:所有逻辑都写在同一个类里,随着功能增加,类会变得臃肿不堪。

Bloc 和 Riverpod 的核心思想就是:让状态的变化变得像“状态机”一样清晰可预测。

UI层 => 触发事件(Event) => 业务逻辑处理 => 产生新状态(State) => UI层根据新状态刷新


二、 Bloc 模式:严格的单向数据流

Bloc 模式将应用分为三个核心部分:

  • Events(事件):UI层触发的动作,如 LoginButtonPressed

  • Bloc(业务逻辑组件):接收Events,处理业务逻辑,并输出States。

  • States(状态):应用在某一时刻的表现状态,如 LoginInitialLoginLoadingLoginSuccessLoginFailure

实战:用 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 依然是无懈可击的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

移动端开发者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值