1.什么是bloc
什么是bloc?我的理解是bloc就像是我们安卓中的一个mvp模式。Bloc可以比较轻松地将展示层的代码与业务逻辑分开,从而使您的代码快速,易于测试且可重复使用。它利用dart语法的stream把每一个事件当作一个流发送到bloc中,最后我们在bloc中处理相关逻辑,把最终数据state返回给view显示。
- 为了更好的理解bloc的作用,请看下面行为流转图
- UI产生事件,流转到bloc中。
- bloc收到用户产生的事件,或者进行逻辑处理,或者向服务器发送相应的网络请求。
- 逻辑处理,或者网络请求的响应结果返回到bloc中。
- bloc把相应的结果,转换成UI显示所需的states,返回给UI
- 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的基本结构。
- 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);
}
复制代码
- 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=[];
}
}
复制代码
- 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的处理呢?
- 利用BlocProvider创建bloc实例
创建bloc实例时有一个child参数,在此child及其子widget中我们可以接收state的返回及UI上数据的改变。
注意,我们需要把此处BlocBUilder中的state传到下级组件否则利用BlocProvider发送事件时会报错。
BlocProvider<MainPageBloc>(
create: (BuildContext context) => MainPageBloc(),
///完整代码请点击底部demo地址。
child: ..childWidget,
),
复制代码
- 利用BlocBuilder包裹state更新时要变更的组件。此处利用state里面的list刷新列表。
BlocBuilder<MainPageBloc, MainPageState>(
builder: (BuildContext context, MainPageState state) {
return ListView(
children: _buildList(context, state.list),
);
},
),
复制代码
- 在用户点击 请点击数据啊 button 时,发送请求列表数据的event到bloc中
ElevatedButton(
onPressed: () {
BlocProvider.of<MainPageBloc>(contextX).add(MainPageListEvent());
},
child: Text(
"请求数据啊!",
),
),
复制代码
- 总结
此时我们就吧bloc一整个流程串起来了。
- view点击发送event。
- bloc收到event进行处理后通过yield将state返回
- view收到state进行数据更新
4 利用插件来进行bloc基本结构生成
通过上面的演示,我们看到了bloc基本结构是如何搭建的,那么每次写个功能都需要手动搭建这些东西,不知道大家怎么想?是不是感觉挺烦的呢?
下面给大家介绍一个studio中bloc的插件。
- 插件安装
Android Studio –> Preferences –> Plugins –> 搜索 Bloc 安装即可!
- 插件使用
右击相应文件夹 –> 选择Bloc Class即可
此时输入相应的名称后系统会自动生成我们基本的bloc结构
复杂的自定义过程就这么自动生成了。是不是爽歪歪?
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的一个简化版本。下面是它的流程图
从图里我们可以知道:
- 与bloc相比Cubit没有event的概念,所有的行为都是由ui直接调用bloc的方法来实现的
- 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结构:
- demo代码说明
- main界面利用利用flutter的主题展示全局bloc改变全局样式的能力
- login界面利用简单的登陆功能展示Cubit的使用
- 原始bloc展示原始bloc的使用
- 展示优化后bloc的使用
Bloc API说明
BlocBuilder
- BlocBuider的创建
BlocBuilder是一个Flutter的 widget,在在创建它时需要给他当前使用的Bloc和State作为泛型,同时需要填写builder(构建子组件的函数)为参数。
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
/// 在此函数中我们可以返回我们根据state创建出的widget。以后每收到state改变,都会引起我们组件的变化。
}
)
复制代码
- 可选的bloc
- 创建BlocBuilder对象时,如果提供了bloc参数,那么程序将会从BlocProvider和当前的BuildContext中自动查找bloc。
- 当且仅当您希望提供一个范围仅限于单个部件(widget)且无法通过父代BlocProvider和当前BuildContext访问的块时,才指定Bloc。
BlocBuilder<BlocA, BlocAState>(
//可选,没有提供则从BlocProvider和当前的Context中获取。
bloc: blocA,
builder: (context, state) {
/// 在此函数中我们可以返回我们根据state创建出的widget。以后每收到state改变,都会引起我们组件的变化。
}
)
复制代码
- 可选的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
- BlocProvider 是Flutter部件(widget),它的主要作用是创建bloc,并通过
BlocProvider.of <T>(context)
方法向其子widget提供bloc。 - 我们通常应该使用BlocProvider来创建新的blocs,并将其提供给其余子树。以便于BlocProvider自动处理bloc的关闭。
BlocProvider(
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
复制代码
- lazy参数
默认情况下,BlocProvider会延迟创建bloc,创建操作会在用户调用BlocProvider.of(context)时执行,如果设置为false,则不会延迟创建。
注意,官方是这么说的,我自测没毛效果,可能还是不知道这玩意真正的使用方式吧
BlocProvider(
lazy: false,
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
复制代码
- 创建一个可传递的bloc
- 某些情况下BlocProvider能把一个已经存在的bloc提供给一个新的组件树。我们通常采用下面方式把一个已经存在的bloc提供给路由的下一个界面。这样BlocProvider不会自动关闭该bloc,因为它没有创建它。
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: ScreenA(),
);
复制代码
- 获取上面提供的bloc
// 扩展的方式
context.read<BlocA>();
// 原始方式
BlocProvider.of<BlocA>(context)
复制代码
注意
- BlocBuilder 参数最好不要加bloc 自己new的bloc,否则会产生事件发送到bloc中处理后yield并不能回来更改state的问题。
- bloc 用yield返回的state必须是一个新的对象,否则yield原来的对象,框架内会判定为同一个对象,数据没有变化,从而无法根据State更新界面!