likes
comments
collection
share

【Flutter】用Android MVI架构思想来理解Bloc框架

作者站长头像
站长
· 阅读数 24

前言

之前做 Flutter 项目是由 Getx 入坑的,但是自从使用过 Bloc 框架之后,我觉得它更适合 Android 开发者。

之前我们做 Android 开发者使用的 MVI 的框架,把数据流变成了单向流动,把状态集中管理形成唯一可信数据源。分为 Intent/Event 事件和 Status 状态。再根据 Flow/LiveData 做UI的变化管理。

切换到 Flutter 项目的 Bloc 框架上,此框架上就限制了我们的使用规范,让我们定义事件和状态,根据BlocListener/BlocConsumer 等做UI的变化管理。

简直是如出一辙,Androider 零门槛入门。并且由于本身 MVI 的思想就是来源自前段,可以说前段开发者也是很轻松的入门,也难怪成为“四大金刚”之一。

本文就简单的介绍一下 Bloc 的使用,如何按照 MVI 的方式理解 Bloc 的各种定义。

一、简单的Bloc的使用与接收器

flutter_bloc 中 Bloc 的定义常规有两种,CuBit 和 Bloc ,我们先说简单模式,简单模式指的是使用 Cubit 来进行状态管理。 Cubit 是一个简化版的 Bloc,用于处理不需要基于事件的简单状态管理。

【Flutter】用Android MVI架构思想来理解Bloc框架

Bloc 的简单定义

我们可以使用 CuBit 来简单的管理简单的状态

例如:

import 'package:flutter_bloc/flutter_bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

当然我们想要用 Cubit 管理对象也是可以的:

class CounterState extends Equatable {
  final int counterValue;
  final bool wasIncremented;

  CounterState({required this.counterValue, required this.wasIncremented});

  @override
  List<Object> get props => [counterValue, wasIncremented];
}

class Counter2Cubit extends Cubit<CounterState> {
  Counter2Cubit() : super(CounterState(counterValue: 0, wasIncremented: true));

  void increment() => emit(CounterState(counterValue: state.counterValue + 1, wasIncremented: true));
  void decrement() => emit(CounterState(counterValue: state.counterValue - 1, wasIncremented: false));
}

下面介绍一下 Bloc 的其他 API:

Bloc 的提供者

BlocProvider它提供了一种方式来将Bloc或Cubit传递给一个小部件树。任何子小部件都可以通过context来获取这个Bloc或Cubit。

MultiBlocProvider 是BlocProvider的一个方便的封装,允许您一次性向子树提供多个Bloc或Cubit。

RepositoryProvider 类似于 BlocProvider,但它不是用于提供 Bloc 或 Cubit,而是用于提供数据仓库或任何类型的数据服务。通过使用 RepositoryProvider,您可以在需要的地方注入数据源,以便它们可以在 widget 树中的任何地方被访问。

Bloc 的获取方式

context.read(): 用于获取 Bloc 或 Cubit 的实例,但不监听变化。常用于触发动作(如按钮点击)时。

context.watch(): 监听 Bloc 或 Cubit 的状态变化,并重建调用 watch 的 widget。

context.select(): 监听 Bloc 或 Cubit 状态的特定部分的变化,仅当所选部分改变时重建。

BlocProvider.of(context): 类似于 context.read(),用于获取 Bloc 或 Cubit 的实例。不推荐在 BlocBuilder、BlocListener 的构建上下文中使用,因为可能会导致访问过时的 context。

Bloc 的接收者

BlocBuilder: 用于构建 UI,根据 Bloc 或 Cubit 的状态变化来重建 widget。适用于 state 变化时需要更新 UI 的场景。

BlocListener: 用于执行一次性操作,如显示弹窗、导航至另一个页面等。它不会重建 UI,而是监听状态变化并响应这些变化。

BlocConsumer: 是 BlocBuilder 和 BlocListener 的组合。可以同时构建 UI 并响应状态变化进行一次性操作。

BlocSelector: 类似于 BlocBuilder,但允许你仅从整个状态中选择一个特定部分来重建。它提供了一种方式来仅在那部分状态变化时重建 UI,而忽略其他状态的变化。

这些都是比较基础的概念,关于这方面的示例代码有兴趣的可以看看我之前的文章 【状态管理插件的”四大天王“简单原理与使用方式对比】

二、标准Bloc的使用示例

以登录页面为例:

sign_in_event.dart:

abstract class SignInEvent {
  const SignInEvent();
}

class EmailEvent extends SignInEvent {
  final String email;
  const EmailEvent(this.email);
}

class PasswordEvent extends SignInEvent {
  final String password;
  const PasswordEvent(this.password);
}

sign_in_states.dart:

class SignInState {
  final String email;
  final String password;

  const SignInState({
    this.email = '',
    this.password = '',
  });

  SignInState copyWith({
    String? email,
    String? password,
  }) {
    return SignInState(
      email: email ?? this.email,
      password: password ?? this.password,
    );
  }
}

sign_in_blocs:

class SignInBloc extends Bloc<SignInEvent, SignInState> {
  SignInBloc() : super(const SignInState()) {
    on<EmailEvent>(_emailEvent);

    on<PasswordEvent>(_passwordEvent);
  }

  void _emailEvent(EmailEvent event, Emitter<SignInState> emit) {
    emit(state.copyWith(email: event.email));
  }

  void _passwordEvent(PasswordEvent event, Emitter<SignInState> emit) {
    emit(state.copyWith(password: event.password));
  }
}

可以把逻辑写在 Bloc 类中,也可以另外分出一个Controller类来做逻辑:

sign_in_controller.dart:

class SignInController {
  final BuildContext context;
  const SignInController({
    required this.context,
  });

  void handleSignIn(String type) async {
    try {
      if (type == 'email') {
        final state = context.read<SignInBloc>().state;
        String emailAddress = state.email;
        String password = state.password;
        if (emailAddress.isEmpty) {
          //
          toastInfo(msg: 'Fill email address.');
          return;
        }
        if (password.isEmpty) {
          //
          toastInfo(msg: 'Fill password.');
          return;
        }

        try {
          final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
            email: emailAddress,
            password: password,
          );
          if (credential.user == null) {
            //
            toastInfo(msg: 'user does not exist');
            return;
          }
          if (!credential.user!.emailVerified) {
            //
            toastInfo(msg: 'You need to verify your email account');
            return;
          }

          var user = credential.user;
          if (user != null) {
            print(('user exist'));
            Global.storageService.setString(
              AppConstants.STORAGE_USER_TOKEN_KEY,
              '12345678',
            );

            if (!context.mounted) return;
            Navigator.of(context).pushNamedAndRemoveUntil('/application', (route) => false);
          } else {
            toastInfo(msg: 'Currently you are not a user of this app');
            return;
          }
        } on FirebaseAuthException catch (e) {
          if (e.code == 'user-not-found') {
            print('No user found for that email');
            toastInfo(msg: 'No user found for that email');
            return;
          } else if (e.code == 'wrong-password') {
            print('Wrong password provided for that user');
            toastInfo(msg: 'Wrong password provided for that user');
            return;
          } else if (e.code == 'invalid-email') {
            print('Your email format is wrong');
            toastInfo(msg: 'Your email format is wrong');
            return;
          }
        }
      }
    } catch (e) {
      print(e.toString());
    }
  }
}

在 Controller 类中传递 BuildContext 对象,然后通过 Context 获取到对应的 Bloc 对象再获取到它的 State 状态直接读取就可以获取到对应的值进行登录操作了

实际页面就是:

class SignIn extends StatefulWidget {
  const SignIn({super.key});

  @override
  State<SignIn> createState() => _SignInState();
}

class _SignInState extends State<SignIn> {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SignInBloc, SignInState>(
      builder: (context, state) {
        return Container(
          color: Colors.white,
          child: SafeArea(
            child: Scaffold(
              appBar: buildAppBar('Log in'),
              body: SingleChildScrollView(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    buildThirdPartyLogin(context),
                    Center(
                      child: reusableText('Or use your email account to login'),
                    ),
                    Container(
                      margin: EdgeInsets.only(top: 36.h),
                      padding: EdgeInsets.only(left: 25.w, right: 25.w),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          reusableText('Email'),
                          SizedBox(height: 5.h),
                          buildTextField('Enter your email address', 'email', 'user', (value) {
                            context.read<SignInBloc>().add(EmailEvent(value));
                          }),
                          reusableText('Password'),
                          SizedBox(height: 5.h),
                          buildTextField('Enter your password', 'password', 'lock', (value) {
                            context.read<SignInBloc>().add(PasswordEvent(value));
                          })
                        ],
                      ),
                    ),
                    forgotPassword(),
                    SizedBox(height: 70.h),
                    buildLogInAdnRegButton('Log in', 'login', () {
                      SignInController(context: context).handleSignIn('email');
                    }),
                    buildLogInAdnRegButton('Sign up', 'register', () {
                      Navigator.of(context).pushNamed('/register');
                    }),
                  ],
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

只是发送事件,把当前的输入框的值赋值给 State,然后在登录的时候调用 Controller 去执行对应的逻辑。

效果:

【Flutter】用Android MVI架构思想来理解Bloc框架

当然其实 Bloc 有很多小变种,比如觉得 State 每次都需要写 copywith 比较麻烦,可以使用自定义 copy 的方案。

class SettingState {
  late int selectedIndex;
  late bool isExtended;

  SettingState init() {
    return SettingState()
      ..selectedIndex = 0
      ..isExtended = false;
  }

  SettingState clone() {
    return SettingState()
      ..selectedIndex = selectedIndex
      ..isExtended = isExtended;
  }
}

在 Bloc 类中就可以这么用

  void _selectTab(SelectEvent event, Emitter<SettingState> emit,
  ) {
    state.selectedIndex++;
    emit(state.clone());
  }

这样也是类似的效果,主要是看你喜欢哪一种方案了,顺便说下使用 copywith 的方案也是可以用编辑器插件生成的,都很方便哦。

并且不管是标准的 Bloc 还是 Cubit 都是可以通过编辑器插件生成相关的文件的,并不麻烦。

三、MVI的思路来理解Bloc的各种概念

什么叫 MVI 模式,其实就是一种严格模式,框架中 MVC 和 MVP 和 MVVM 一路走来是也算是越严格,但是到 MVI 才算是真正的严格。页面,逻辑,状态,行为。这四点真正的分离出来并且做出限制,必须遵循这种规格,是大型应用多人协同开发的神器。

接下来我们就看看 MVI 中的各种概念与 Bloc 中的实现如何一一对应。

import 'package:flutter_bloc/flutter_bloc.dart';
import 'home_page_event.dart';

import 'home_page_state.dart';

class HomePageBloc extends Bloc<HomePageEvent, HomePageState> {
  HomePageBloc() : super(const HomePageState()) {
    //监听事件的分发,在 Kotlin MVI 中是通过 Channel/Flow 来实现分发事件的。
    on<HomePageDots>(_homePageDots);
  }

  void _homePageDots(HomePageDots event, Emitter<HomePageState> emit) {

    //发送事件,在Kotlin MVI 中是通过 dataclass 的 copy 实现更新,通过 StateFlow 进行传递消息。
    emit(state.copyWith(index: event.index));
  }
}

//相当于Kotlin MVI 的 Intent/Event 事件,类似密封类,可以携带参数
abstract class HomePageEvent {
  const HomePageEvent();
}

class HomePageDots extends HomePageEvent {
  final int index;
  HomePageDots(this.index);
}

//相当于Kotlin MVI 的 状态state, Kotlin 的 dataclass
class HomePageState {
  const HomePageState({this.index = 0});
  final int index;

  //自定义Kotlin dataclass 的 copy 方法,拷贝一个新对象返回。
  HomePageState copyWith({int? index}) {
    return HomePageState(index: index ?? this.index);
  }
}

这里给出了注释,其实本质的思路是一样的,学习了 MVI 思路之后再看 Bloc 就不会觉得复杂了。

在页面中接收状态与发送事件:

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 使用BlocProvider来提供HomePageBloc
    return BlocProvider<HomePageBloc>(
      create: (context) => HomePageBloc(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Home Page'),
        ),
        body: BlocBuilder<HomePageBloc, HomePageState>(
          builder: (context, state) {
            // 这里可以根据HomePageState来构建你的UI
            // 例如,可以使用state.index来决定显示哪个页面或者内容
            return Center(
              child: Text('当前索引是:${state.index}'),
            );
          },
        ),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: context.select((HomePageBloc bloc) => bloc.state.index),
          items: const [
            BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
            BottomNavigationBarItem(icon: Icon(Icons.business), label: 'Business'),
            // 更多的项目...
          ],
          onTap: (index) {
            // 当用户点击BottomNavigationBar时,我们向Bloc发送一个HomePageDots事件
            context.read<HomePageBloc>().add(HomePageDots(index));
          },
        ),
      ),
    );
  }
}

到这里已经可以用了,但是我们需要注意的时候我们在真实使用的时候往往有很多变种的用法。

abstract class SignInEvent extends Equatable{
  const SignInEvent();

  @override
  List<Object> get props => [];
}

class EmailEvent extends SignInEvent {
  final String email;
  const EmailEvent(this.email);

  @override
  List<Object> get props => [email];
}

class PasswordEvent extends SignInEvent {
  final Completer<void> completer;
  final String password;
  const PasswordEvent(this.password,this.completer);

  @override
  List<Object> get props => [completer,password];
}

例如我们常常会使用 Equatable 让 Event可以更好的判断类型,相同的值不需要重复发送事件。

再比如我们一些用法会把 State 也分为“密封类”的写法,可以是不同的状态类标识不同的UI状态,在不同的状态类中使用DataClass标识这个UI状态下的具体状态值。

abstract class UpdateState extends Equatable {
  const UpdateState();
}

class NoUpdateState extends UpdateState {
  final bool isChecked;
  final int checkTime;

  const NoUpdateState({this.isChecked = false, this.checkTime = 0});

  @override
  List<Object?> get props => [isChecked, checkTime];
}

class CheckLoadingState extends UpdateState {
  const CheckLoadingState();
  @override
  List<Object?> get props => [];
}

class DownloadingState extends UpdateState {
  final double progress;
  final int appSize;

  const DownloadingState({required this.progress, required this.appSize});

  @override
  List<Object?> get props => [progress, appSize];
}

class CheckErrorState extends UpdateState {
  final String error;

  const CheckErrorState({required this.error});

  @override
  List<Object?> get props => [error];

  @override
  String toString() {
    return 'CheckErrorState{error: $error}';
  }
}

class ShouldUpdateState extends UpdateState {
  final String oldVersion;
  final AppInfo info;

  const ShouldUpdateState({required this.oldVersion, required this.info});

  @override
  List<Object?> get props => [oldVersion, info];

  @override
  String toString() {
    return 'ShouldUpdateState{oldVersion: $oldVersion, info: $info}';
  }
}

当然更多的还是简单的状态写法:

class SignInState {
  final String email;
  final String password;

  const SignInState({
    this.email = '',
    this.password = '',
  });

  SignInState copyWith({
    String? email,
    String? password,
  }) {
    return SignInState(
      email: email ?? this.email,
      password: password ?? this.password,
    );
  }
}

总的来说在使用Bloc或其他状态管理库时,状态(State)可以被表示为两种风格:

简单的DataClass(数据类): 这通常是一个包含所有相关数据的单个类。这个类可能有方法如copyWith,这样你就可以改变对象的部分字段,而不是每次都需要创建一个全新的实例。这种方式通常适用于相对简单的UI状态,或者当你的状态不会经常改变时。

(密封类)标识不同的UI状态: 在这种方法中,你会为每一个独特的UI状态创建一个单独的类。这些状态类可以继承自一个共同的基类或者实现同一个接口,并且每个类都可以包含特定于该状态的数据和行为。这种模式更类似于传统的面向对象的状态模式,它允许你利用多态和模式匹配(如在switch-case语句中)来简化状态管理逻辑。当应用中的状态较为复杂或有明显的区分时,这种方法特别有用。

其实这一点也是和 Android 的 MVI 架构很类似,在之前的 Android MVI 文章中我也提到过,State 也是可以用数据类定义也可以用密封类定义,密封类内部再用数据类,也差不多是这样的思路。

四、关于 Bloc 的常用疑问:

  1. Bloc的状态是如何刷新的?脱离了原生的setState() 刷新方式吗?有哪些优缺点?性能如何?

在使用 Bloc 模式时,状态的更新与原生Flutter的setState()方法不同。在Bloc模式中,UI组件监听Bloc或Cubit发出的状态流,当状态发生变化时,UI会根据新的状态重建。

流程如下:用户交互或程序逻辑触发事件。Bloc接收到事件,并根据事件和当前状态进行业务逻辑处理。Bloc产生一个新的状态对象。新的状态通过Bloc的状态流(Stream)发出。UI组件通过BlocBuilder、BlocListener或BlocConsumer监听Bloc的状态流。当新状态到达时,监听状态流的UI组件将重新构建以反映新的状态。

Bloc模式的性能通常很好,特别是在大型应用或需要复杂状态管理的应用中。由于它只重建依赖于特定状态的widgets,因此避免了不必要的widget重建,这有助于保持高性能。

优点很多,主要是Bloc模式强调了逻辑和UI的分离,有助于更清晰的代码结构,易于测试和维护,然后就是其性能优势。

缺点也很明确,就是复杂,模板代码更多,学习曲线陡峭。

当然我们需要一些小技巧,比如BlocBuild中的buildwhen根据状态判断重建啊,或者context.select()的选择状态重建等等操作可以避免不必要的重建,从而提高性能。

  1. 我看一些开源项目,有些项目是在项目入口中定义bloc的providers,全局直接管理了全部的Bloc类,有些项目是在不同的页面手动的创建各自的provide,哪一种是值得推荐的��

关于在 Flutter 项目中管理 BLoc 对象的方式,没有一种绝对的最佳实践,通常取决于项目的复杂程度和开发人员的个人偏好。不过,这里有一些建议可以参考:

全局管理 BLoc 在应用程序入口处全局管理所有 BLoc 对象,这种方式的优点是方便维护和共享 BLoc 实例。但缺点是会增加应用程序的启动时间,而且所有 BLoc 对象在应用程序生命周期内都会存在,可能导致不必要的内存占用。

适合场景:中小型项目,BLoc 数量有限,多个页面需要共享相同的 BLoc 状态。

按需创建 BLoc 在每个需要使用 BLoc 的页面或组件中,手动创建和管理 BLoc 实例。这种方式的优点是只在需要时才创建 BLoc 对象,可以减少不必要的内存占用。但缺点是需要手动管理 BLoc 的生命周期,代码量会增加。

适合场景:大型项目,BLoc 数量多,不同页面之间状态隔离,不需要共享 BLoc

混合使用 对于一些需要在整个应用程序中共享的重要状态,可以在入口处全局管理相应的 BLoc。而对于每个页面或组件特有的状态,则按需创建和管理对应的 BLoc。

适合场景:项目规模较大,有一些全局状态和大量局部状态需要管理。

个人建议混合使用,对于全局的 BLoc 我们可以用于一些特有的功能比如用户信息的刷新,用户状态更新,消息未读的数量等全局适用的逻辑我们可以用全局管理的方式,但是对于页面对应的 BLoc 还是推荐使用按需创建的方式。

  1. 我使用Getx 框架,我的 Controller都是自动注入,关闭页面之后自动释放回收的,我使用 BLoc 框架之后,我的页面对应的 Bloc 类不需要销毁吗?

当用户离开当前页面并且页面被销毁时,当前页面持有的Bloc也会自动被清理(如果没有其他地方引用它)这样可以避免不必要的资源消耗并保持应用的性能。

前提是你使用的是 Flutter_bloc 库中的 BlocProvider,那么它提供了一种便捷的方式来自动关闭和销毁 BLoc 对象。您只需要在不再需要该 BLoc 时将其从 Widget 树中移除即可,BlocProvider 会自动处理关闭和销毁操作。

如果您直接创建了 BLoc 对象,那么在不再需要该 BLoc 时,您需要手动调用它的 close() 方法来关闭它。通常您可以在 dispose 方法中执行这个操作。

  1. StatefulWidget 或 StatelessWidget 都能自动释放吗?为什么和Getx的原理不同? Bloc 是基于什么原理实现的自动释放?

BlocProvider会自动调用Bloc的close方法当它被从widget树中移除时。这通常发生在页面被pop时。这意味着如果你正确地使用了BlocProvider来提供HomePageBloc到你的HomePage,并且没有其他地方引用这个Bloc,那么当HomePage被销毁时,HomePageBloc也会自动被清理。

无论你在StatefulWidget还是在StatelessWidget中使用BlocProvider,Bloc的自动释放机制都是相同的。这是因为BlocProvider的自动清理能力并不依赖于它被用在有状态(widget)还是无状态(widget)的组件中,而是依赖于BlocProvider自身的实现以及它是如何与Flutter的widget生命周期交互的。

自动释放的关键点:

Widget树生命周期:自动释放的关键在于BlocProvider是否从widget树中被移除,而不是它被包含在哪种类型的widget中。

BlocProvider的实现:BlocProvider通过在其dispose方法中调用Bloc的close方法来自动清理Bloc。这一过程是自动执行的,前提是BlocProvider是通过正常的widget生命周期被移除的。

路由变化:在Flutter应用中,页面之间的导航通常会导致widget树的重新构建。当导航从一个页面移动到另一个页面,并且第一个页面使用了BlocProvider时,如果这个页面的widget被彻底移除了,那么BlocProvider也会被移除,并触发Bloc的自动清理。

五、自动化-使用依赖注入的方式管理Bloc

使用依赖注入的方式(例如GetIt)管理 Bloc 的对象,其实也是很常见的,我参考一些开源项目很多人用,当然也有很多人不用。

优点很明显。

  1. 解耦和灵活性:依赖注入提高了代码的解耦性,使得Bloc的使用和测试更加灵活。通过DI,Bloc可以在需要时被轻松地替换或模拟,这对于单元测试和功能测试尤其有用。
  2. 管理依赖关系:使用DI框架如GetIt可以帮助开发者集中管理依赖关系,使得依赖的配置和管理更加集中和一致。这降低了手动管理Bloc实例及其依赖项的复杂性。
  3. 懒加载和单例管理:GetIt等DI框架通常提供懒加载和单例管理功能,这意味着Bloc实例可以按需创建,并且可以保证全局唯一性,从而优化资源使用和性能。

缺点也很明显,就是添加了项目的学习成本和增加了项目的复杂度。

如果我们选择 GetIt 这个插件来管理的话,其实我们可以通过 injectable 这样的插件配合 build_runner 来实现自动生成依赖表。

先定义我们的初始化代码

final getIt = GetIt.instance;

@InjectableInit(
  initializerName: 'init', // default
  preferRelativeImports: true, // default
  asExtension: true, // default
)
void configureDependencies() => getIt.init();

定义我们的Bloc注入,此时会自动查找 ArticleRepository 的依赖注入。

@Injectable()
class HomeBloc extends BaseBloc<HomeEvent, HomeState> {
     HomeBloc(this._repository) : super(HomeState()) {
     ... 
  }
  final ArticleRepository _repository;

}

//我们可以指定我们的数据仓库为单例。

@singleton
class ArticleRepository{
  ...
}

运行命令 dart run build_runner build 会生成对应的 injection.config.dart 文件,我们不需要改动。

在使用的时候:

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  HomeBloc homeBloc =  GetIt.I.get<HomeBloc>();

  @override
  Widget build(BuildContext context) {
    // 使用BlocProvider来提供HomePageBloc
    return BlocProvider<HomePageBloc>(
      create: (context) => homeBloc,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Home Page'),
        ),
        body: BlocBuilder<HomePageBloc, HomePageState>(
          builder: (context, state) {
            // 这里可以根据HomePageState来构建你的UI
            // 例如,可以使用state.index来决定显示哪个页面或者内容
            return Center(
              child: Text('当前索引是:${state.index}'),
            );
          },
        ),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: context.select((HomePageBloc bloc) => bloc.state.index),
          items: const [
            BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
            BottomNavigationBarItem(icon: Icon(Icons.business), label: 'Business'),
            // 更多的项目...
          ],
          onTap: (index) {
            // 当用户点击BottomNavigationBar时,我们向Bloc发送一个HomePageDots事件
            context.read<HomePageBloc>().add(HomePageDots(index));
          },
        ),
      ),
    );
  }
}

在 HomePage 中通过 GetIt 找到并设置给 BlcoProvide ,并且 Bloc 和 GetIt 都是当前页面生效的,当页面关闭,对象不再持有都是会自动回收的,两者完美配合。

六、自动化-使用freezed来自动化管理bloc中state与event对象

和依赖注入一样其实这一个点也是非必须的,有些项目喜欢用 freezed ,有些项目喜欢用 equatable,还有些项目是不用的自己做判断。

使用 freezed 或 equatable 主要是为了数据类支持,不可变性,状态比对。

我们在开发中可能需要用到比对,比如用 when 当什么状态展示什么布局,用了这些框架之后对象比较基于对象的属性值而不是引用。

而对于数据类的支持,他们可以生成额外的代码提供了类似Kotlin中数据类(data class)的功能。这包括不可变性(immutable),以及内置的复制方法、toString方法、以及基于属性的equals和hashCode方法。这使得状态和事件的管理更加方便和安全。

总的来说他们是有助于提高应用性能。只有当状态确实发生变化时,UI才会重建,避免了不必要的渲染过程。当然你可以不使用这些框架自己在代码的环节自己处理也是可以的。

只是 freezed 的功能更强大,而 equatable 使用更便捷,他们的侧重点不同。由于 equatable 的使用很简单,我就介绍一下 freezed 的简化使用,配合 build_runner 自动生成代码简化编码流程。

添加对应的依赖之后


  # https://pub.dev/packages/freezed_annotation
  freezed_annotation: ^2.4.1
  json_annotation: ^4.9.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0
  # https://pub.dev/packages/build_runner
  build_runner: ^2.4.9
  # https://pub.dev/packages/freezed
  freezed: ^2.5.1
  # https://pub.dev/packages/json_annotation
  json_serializable: ^6.8.0

我们在指定的对象上加上对应的注解。

part 'id_name.freezed.dart';
part 'id_name.g.dart';

@freezed
class IdNameBean with _$IdNameBean {
  const factory IdNameBean({
    String? id,
    String? name,
  }) = _IdNameBean;
}

这里是模板代码推荐使用模板生成,然后填写指定的属性即可。

此时配合 Bloc 框架我们就能很方便的定义对应的 Event 和 State 啦!

home_event.dart:

import 'package:freezed_annotation/freezed_annotation.dart';
part 'home_event.freezed.dart';

abstract class HomeEvent extends BaseBlocEvent {
  const HomeEvent();
}

@freezed
class HomePageInitiated extends HomeEvent with _$HomePageInitiated {
  const factory HomePageInitiated() = _HomePageInitiated;
}

home_state.dart:

import 'package:freezed_annotation/freezed_annotation.dart';
part 'home_state.freezed.dart';

@freezed
class HomeState extends BaseBlocState with _$HomeState {
  factory HomeState({
    @Default([]) List<PlayListItemData> playList,
  }) = _HomeState;
}

当然了,就算我们不用 freezed 直接用 Equatable + dataclass 插件可是可以的,或者我们直接用 dataclasss 插件直接生成代码也是可以的。

或者更简单不使用插件直接代码定义也是可以的,例如:

class SettingState{

  late int selectedIndex;
  late bool isExtended;

  SettingState init() {
    return SettingState()
      ..selectedIndex = 0
      ..isExtended = false;
  }

  SettingState clone() {
    return SettingState()
      ..selectedIndex = selectedIndex
      ..isExtended = isExtended;
  }

}

我们不需要生成 copyWith ,直接自定义一个 clone 方法,在 Bloc 类中发射状态的时候使用 clone 即可。

class SettingBloc extends Bloc<SettingEvent, SettingState> {
  SettingBloc() : super(SettingState().init()) {
    on<InitEvent>(_init);
  }

  void _init(InitEvent event, Emitter<SettingState> emit) async {
    emit.selectedIndex ++;
    emit(state.clone());
  }
}

这样也是生成了一个新的对象,只是在页面状态判断的时候我们就需要手动的根据值来判断,因为没有使用 Equatable 或者重写 equals 和 hashCode 方法,判断对象是不准确的。

总结

本文介绍了 Bloc 的简单使用方式和标准使用方式,并且用 Android MVI 架构的思路来理解 Blco 的标准模式。

可以说 Blco 的思路是和 MVI 的思想很相似的(其实整个前端都差不多) ,而使用 Cubit 简化版本就和我们常见的 MVVM 思路很相似,如果是从 Android 开发者转换过来也是很容易上手的。

其次我们讲到一些依赖注入和 dataclass 相关的处理,以及配合 build_runner 实现自动化生成代码简化流程的示例。

文章篇幅很长,文章很多观点都是我自己的思考,难免有错漏,如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。

如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

由于不是具体的 Demo ,本文代码都已在文中贴出作为参考。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。

【Flutter】用Android MVI架构思想来理解Bloc框架

转载自:https://juejin.cn/post/7373964489681469503
评论
请登录