Flutter中的bloc模式解析

1.什么是bloc

什么是bloc?我的理解是bloc就像是我们安卓中的一个mvp模式。Bloc可以比较轻松地将展示层的代码与业务逻辑分开,从而使您的代码快速,易于测试且可重复使用。它利用dart语法的stream把每一个事件当作一个流发送到bloc中,最后我们在bloc中处理相关逻辑,把最终数据state返回给view显示。

  • 为了更好的理解bloc的作用,请看下面行为流转图

image.png

  1. UI产生事件,流转到bloc中。
  2. bloc收到用户产生的事件,或者进行逻辑处理,或者向服务器发送相应的网络请求。
  3. 逻辑处理,或者网络请求的响应结果返回到bloc中。
  4. bloc把相应的结果,转换成UI显示所需的states,返回给UI
  5. UI收到bloc中返回的states从而更新页面。

那么问题来了。我们看这个bloc是不是有点感觉像是之前说过的dart异步中的stream,ui往流中扔东西,然后bloc接收到了ui扔的东西,进行下一步处理。
同时bloc处理Ui扔过来的东西后又通过另外一个流,但是注意此时bloc是上游,它往流中扔东西,ui接到东西并更新界面。

此时我们可以这样理解,小王向生产线大喊(events),我要新衣服。生产线负责人(bloc)接收到命令去买针买线制作衣服(states)。制作好了之后生产线在另一头吧衣服运给小王。小王接收到新衣服美滋滋的穿上了。

2.bloc的简单使用

通过上面的介绍,希望大家能对bloc这个框架有一个简单的理解。接下来我们来了解下bloc的简单使用。无论学什么东西,我们从知道,到简单使用,再到熟练,无论如何都需要先用一用,等简单的应用掌握了,遇到问题,再仔细查一查,基本上就ok了。

1 配置

  • 添加bloc依赖

对于Flutter用户,我们只需要把依赖添加进pubspec.yaml中就可以使用bloc强大的功能啦。

dependencies:
  flutter_bloc: ^7.0.0
复制代码
  • 下载bloc依赖库

运行 flutter packages get 以使工程下载到依赖包

  • 导包并使用

对于Flutter应用程序,我们只要导入flutter_bloc。就可以自由使用啦。

import 'package:flutter_bloc/flutter_bloc.dart';
复制代码

2 Bloc基本结构的搭建

从上文的行为流转图我们知道,bloc中最重要的几个概念是,event,bloc,及 state 那么我们现在从0开始。搭建一个bloc的基本结构。

  1. event

此处定义了两个event。MainPageEvent是event基类,MainPageListEvent表示我们要请求MainPage列表,MainPageLikeEvent表示我们要进行点赞行为。

part of 'main_page_bloc.dart';

@immutable
abstract class MainPageEvent {}

class MainPageListEvent extends MainPageEvent{

}

class MainPageLikeEvent extends MainPageEvent{
  final int position;
  final bool isLike;
  MainPageLikeEvent(this.position,this.isLike);
}


复制代码
  1. state

此处定义了一个bloc返回给UI界面的一个state,Ui处收到此state后会自动更新界面

part of 'main_page_bloc.dart';

class MainPageState {
  List<bool> list;
  MainPageState clone(){
    return MainPageState()..list=this.list;
  }

  MainPageState init(){
    return MainPageState()..list=[];
  }
}
复制代码
  1. bloc

bloc是我们整个bloc框架的核心,它起到承上启下的作用。当event事件发送到bloc后,他需要去处理逻辑或请求,同时把结果(state)通过流的形式返回给UI从而使界面更新。

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

part 'main_page_event.dart';

part 'main_page_state.dart';

class MainPageBloc extends Bloc<MainPageEvent, MainPageState> {
  MainPageBloc() : super(MainPageState().init());

  @override
  Stream<MainPageState> mapEventToState(MainPageEvent event) async* {
    if(event is MainPageListEvent){
      yield state.clone()..list=List.filled(30, false);
    }else if(event is MainPageLikeEvent){
      yield state.clone()..list[event.position]=event.isLike;
    }
  }
}


复制代码

3 bloc在view中如何使用?

通过上面步骤,我们已经把bloc所需的基本结构搭建完成了,那么如何在view使用bloc,是我们能真正的使用此结构进行事件的发送,及返回state的处理呢?

  1. 利用BlocProvider创建bloc实例

创建bloc实例时有一个child参数,在此child及其子widget中我们可以接收state的返回及UI上数据的改变。
注意,我们需要把此处BlocBUilder中的state传到下级组件否则利用BlocProvider发送事件时会报错。

BlocProvider<MainPageBloc>(
  create: (BuildContext context) => MainPageBloc(),
  ///完整代码请点击底部demo地址。
  child: ..childWidget,
),
复制代码
  1. 利用BlocBuilder包裹state更新时要变更的组件。此处利用state里面的list刷新列表。
BlocBuilder<MainPageBloc, MainPageState>(
  builder: (BuildContext context, MainPageState state) {
    return ListView(
      children: _buildList(context, state.list),
    );
  },
),
复制代码
  1. 在用户点击 请点击数据啊 button 时,发送请求列表数据的event到bloc中
ElevatedButton(
  onPressed: () {
    BlocProvider.of<MainPageBloc>(contextX).add(MainPageListEvent());
  },
  child: Text(
    "请求数据啊!",
  ),
),
复制代码
  1. 总结

此时我们就吧bloc一整个流程串起来了。

  • view点击发送event。
  • bloc收到event进行处理后通过yield将state返回
  • view收到state进行数据更新

4 利用插件来进行bloc基本结构生成

通过上面的演示,我们看到了bloc基本结构是如何搭建的,那么每次写个功能都需要手动搭建这些东西,不知道大家怎么想?是不是感觉挺烦的呢?

下面给大家介绍一个studio中bloc的插件。

  1. 插件安装

Android Studio –> Preferences –> Plugins –> 搜索 Bloc 安装即可!
image.png

  1. 插件使用

右击相应文件夹 –> 选择Bloc Class即可
image.png

此时输入相应的名称后系统会自动生成我们基本的bloc结构

复杂的自定义过程就这么自动生成了。是不是爽歪歪?

image.png

5 自动生成的state在使用过程中的一些问题

  • 问题1 缺少state初始化使bloc中编码比较复杂
  • 问题2 每次更改其中一个变量时我们需要对其他所有变量进行一次赋值,否则会丢失其他变量的值。
  • 问题3 @immutable注解降低了state的操作灵活性,而系统又不支持state对象地址不变的情况下更新数据。哪怕state对象里面的字段赋值变化了,yield后依旧不能提示UI更新。

6 优化后的state

此时我们在state类中增加init和clone方法,他们的返回值都是自身类的对象。并去掉immuable注解。

part of 'main_page_bloc.dart';

class MainPageState {
  List<bool> list;
  MainPageState clone(){
    return MainPageState()..list=this.list;
  }

  MainPageState init(){
    return MainPageState()..list=[];
  }
}

复制代码

优化后的bloc

通过state中新增init和clone方法,使bloc中我们对state的初始化和拷贝数据变得相当简单。此时我们代码可以写成如下结构,假如说只改变其一的话,我们也能很轻松的通过clone函数完成其所有属性拷贝。

注,此处代码与上面的代码相同


import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

part 'main_page_event.dart';

part 'main_page_state.dart';

class MainPageBloc extends Bloc<MainPageEvent, MainPageState> {
  MainPageBloc() : super(MainPageState().init());

  @override
  Stream<MainPageState> mapEventToState(MainPageEvent event) async* {
    if(event is MainPageListEvent){
      yield state.clone()..list=List.filled(30, false);
    }else if(event is MainPageLikeEvent){
      yield state.clone()..list[event.position]=event.isLike;
    }
  }
}

复制代码

3. Cubit介绍

Cubit是Bloc的一个简化版本。下面是它的流程图

image.png

从图里我们可以知道:

  1. 与bloc相比Cubit没有event的概念,所有的行为都是由ui直接调用bloc的方法来实现的
  2. bloc中的方法被调用后进行相应逻辑处理或者网络请求,并调用emit方法把结果返回给state

由于Cubit是bloc的简化版,我们前面已经理解了bloc的相关过程。所以这里直接上Cubit操作代码。

假如我现在有一个功能,根据用户id获取用户信息。并跳转界面

  • 定义要显示的state
part of 'login_page_cubit.dart';

class LoginPageState{
  String name;
  int age;
  String sex;

  LoginPageState init(){
    return LoginPageState()..name=""..age=0..sex="";
  }

  LoginPageState clone(){
    return LoginPageState()..name=name..age=age..sex=sex;
  }
}
复制代码
  • 定义Cubit

此时我们可以看到Cubit中并没有类似于Bloc中的mapEventToState方法,也没有Event,这里view层面可以直接调用Cubit的相关方法进行逻辑处理,少了Event的传递使代码更加简单。

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';

part 'login_page_state.dart';

class LoginPageCubit extends Cubit<LoginPageState> {
  LoginPageCubit() : super(null);

  void login(BuildContext context,int id){
    emit(LoginPageState()..name="zhangchenzhou$id"..age=id..sex="男");
    Future.delayed(Duration(seconds: 3),).then((value) => Navigator.of(context).pushNamed("/main"));
  }
}

复制代码
  • 使用Cubit

由于逻辑简单,我贴了此view的所有代码。此界面显示了一个输入框和一个按钮,输入框输入用户id之后,点击按钮,则去请求用户相关信息,并显示在当前界面上来。此界面包含了使用Cubit的完整流程,大家可以看到其流程基本上和Bloc使用起来一致。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'login_page_cubit.dart';

class LoginPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _LoginPage();
  }
}

class _LoginPage extends State<LoginPage> {
  TextEditingController _controller;
  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider<LoginPageCubit>(
      create: (BuildContext context) => LoginPageCubit(),
      child: BlocBuilder<LoginPageCubit, LoginPageState>(
        builder: (BuildContext context, LoginPageState state) => Scaffold(
          appBar: AppBar(
            title: Text("loginPage"),
          ),
          body: Column(
            children: [
              Container(
                margin: EdgeInsets.only(
                  top: 20,
                  left: 20,
                  right: 20,
                ),
                child: TextField(
                  controller: _controller,
                  inputFormatters: [FilteringTextInputFormatter.digitsOnly],
                  style: TextStyle(color: Colors.blueAccent),
                  decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: "用户id",
                    hintText: "请输入用户ID(数字)",
                  ),
                  onEditingComplete: () {},
                  onChanged: (v) {},
                  onSubmitted: (v) {},
                ),
              ),
              Visibility(
                child: Padding(
                  padding: EdgeInsets.all(20),
                  child: Text(
                      "姓名:${state?.name}  性别:${state?.sex} 年龄:${state?.age}"),
                ),
                visible: state != null,
              ),
              ElevatedButton(
                onPressed: () {
                  BlocProvider.of<LoginPageCubit>(context)
                      .login(context,int.parse(_controller.text.isNotEmpty ? _controller.text : "0"));
                },
                child: Text("登陆"),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

复制代码
  • 唯一不同的地方

使用Bloc,当我们根据上下文从BlocProvider中获取bloc后是以add(Event event)事件的形式来进行event的分发,而Cubit则是直接调用Cubit类中的方法来执行逻辑处理。

BlocProvider.of<LoginPageCubit>(context).login(context,int.parse(_controller.text.isNotEmpty ? _controller.text : "0"));
复制代码

3. 全局Bloc的使用

全局bloc的使用,我的demo是改变全局actionBar背景其实还有其他许多场景可以使用全局的actionBar期待大家自己发现。

  • 在App内定义全局的BlocProvider
  runApp(
    MultiBlocProvider(
      providers: [
        BlocProvider<ConfigBloc>(
          create: (BuildContext context) {
            return ConfigBloc();
          },
        ),
      ],
      child: MyApp(),
    ),
  );
复制代码
  • 在view中使用定义的state设置appBar的颜色
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ConfigBloc, ConfigState>(
      builder: (BuildContext context, ConfigState state) => MaterialApp(
        routes: Routers.routers,
        initialRoute: "/",
        theme: ThemeData(
          appBarTheme: AppBarTheme(
            color: state.appBarColor,
          ),
          textTheme: TextTheme(
            bodyText1: TextStyle(
              color: state.textColor,
            ),
            bodyText2: TextStyle(
              color: state.textColor,
            ),
          ),
        ),
      ),
    );
  }
复制代码
  • 在其他地方更改appBar颜色
//定义颜色列表
  var colorList = [
    Colors.redAccent,
    Colors.orangeAccent,
    Colors.yellowAccent,
    Colors.yellowAccent,
    Colors.blueAccent,
    Colors.purpleAccent,
    Colors.pinkAccent
  ];
//点击发送appBar颜色更改事件
Row(
  children: colorList
      .map((e) => GestureDetector(
            child: Container(
              color: e,
              width: 30,
              height: 30,
              padding: EdgeInsets.all(10),
              margin: EdgeInsets.all(10),
            ),
            onTap: () {
              BlocProvider.of<ConfigBloc>(context).add(AppBarEvent(e));
            },
          ))
      .toList(),
)
复制代码

4. 多Bloc的处理

MultiBlocProvider

MultiBlocProvider是Flutter小部件,它可将多个BlocProvider小部件合并为一个。 当我们需要创建多个bloc时避免了嵌套。它提高了可读性,通过使用,MultiBlocProvider我们可以。

  • 从:
BlocProvider<BlocA>(
  create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(
    create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
      child: ChildA(),
    )
  )
)
复制代码
  • 到:
MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)
复制代码

5. Demo地址

本文demo地址已上传至git,欢迎下载参考。

  • demo结构:

image.png

  • demo代码说明
    1. main界面利用利用flutter的主题展示全局bloc改变全局样式的能力
    2. login界面利用简单的登陆功能展示Cubit的使用
    3. 原始bloc展示原始bloc的使用
    4. 展示优化后bloc的使用

demo地址

Bloc API说明

BlocBuilder

  1. BlocBuider的创建

BlocBuilder是一个Flutter的 widget,在在创建它时需要给他当前使用的Bloc和State作为泛型,同时需要填写builder(构建子组件的函数)为参数。

BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    /// 在此函数中我们可以返回我们根据state创建出的widget。以后每收到state改变,都会引起我们组件的变化。
  }
)
复制代码
  1. 可选的bloc
    • 创建BlocBuilder对象时,如果提供了bloc参数,那么程序将会从BlocProvider和当前的BuildContext中自动查找bloc。
    • 当且仅当您希望提供一个范围仅限于单个部件(widget)且无法通过父代BlocProvider和当前BuildContext访问的块时,才指定Bloc。
BlocBuilder<BlocA, BlocAState>(
//可选,没有提供则从BlocProvider和当前的Context中获取。
  bloc: blocA, 
  builder: (context, state) {
    /// 在此函数中我们可以返回我们根据state创建出的widget。以后每收到state改变,都会引起我们组件的变化。
  }
)
复制代码
  1. 可选的buildWhen

根据之前的state和现在的state决定是否重新够建widget。

  • 如果condition返回true,将使用state调用builder,并且部件(widget)将重新构建。
  • 如果buildWhen返回false,则不会用state调用builder,也不会进行重建。
BlocBuilder<BlocA, BlocAState>(
  buildWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)
复制代码

BlocProvider

  1. BlocProvider 是Flutter部件(widget),它的主要作用是创建bloc,并通过BlocProvider.of <T>(context)方法向其子widget提供bloc。
  2. 我们通常应该使用BlocProvider来创建新的blocs,并将其提供给其余子树。以便于BlocProvider自动处理bloc的关闭。
BlocProvider(
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);
复制代码
  1. lazy参数

默认情况下,BlocProvider会延迟创建bloc,创建操作会在用户调用BlocProvider.of(context)时执行,如果设置为false,则不会延迟创建。
注意,官方是这么说的,我自测没毛效果,可能还是不知道这玩意真正的使用方式吧

BlocProvider(
  lazy: false,
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);
复制代码
  1. 创建一个可传递的bloc
  • 某些情况下BlocProvider能把一个已经存在的bloc提供给一个新的组件树。我们通常采用下面方式把一个已经存在的bloc提供给路由的下一个界面。这样BlocProvider不会自动关闭该bloc,因为它没有创建它。
BlocProvider.value(
  value: BlocProvider.of<BlocA>(context),
  child: ScreenA(),
);
复制代码
  • 获取上面提供的bloc
// 扩展的方式
context.read<BlocA>();
// 原始方式
BlocProvider.of<BlocA>(context)
复制代码

注意

  1. BlocBuilder 参数最好不要加bloc 自己new的bloc,否则会产生事件发送到bloc中处理后yield并不能回来更改state的问题。
  2. bloc 用yield返回的state必须是一个新的对象,否则yield原来的对象,框架内会判定为同一个对象,数据没有变化,从而无法根据State更新界面!
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享