Flutter开发中状态管理一直是一个重点,仅通过setState()方法来更新状态在复杂的业务逻辑下会变得难以维护。目前主要有三个状态管理库来更好的管理flutter项目上的UI状态,Provider, GetX, BLOC。BLOC相对于其他框架他的状态管理逻辑十分清晰,但对于初学者来说学习成本也比较大,学习起来也比较困难。GetX和Provider则是比较简单易用。希望此篇博客可以帮助到学习BLOC的朋友,一起共同进步。
BLOC的状态管理是基于事件Event和状态State来管理的。
在BLOC的状态管理过程中需要先定义事件Event和状态State。
Event定义
abstract class LoginEvent extends Equatable {
const LoginEvent();
@override
List<Object> get props => [];
}
class LoginSubmitted extends LoginEvent {
const LoginSubmitted(this.phoneNum);
final PhoneNum phoneNum;
@override
List<Object> get props => [phoneNum];
}
State定义
class LoginState extends Equatable {
const LoginState({
this.msg = "",
this.status = FormzStatus.pure,
this.phoneNum = const PhoneNum.pure(),
});
final FormzStatus status;
final PhoneNum phoneNum;
final String msg;
LoginState copyWith({
FormzStatus? status,
PhoneNum? phoneNum,
String? msg
}) {
return LoginState(
status: status ?? this.status,
phoneNum: phoneNum ?? this.phoneNum,
msg: msg ?? this.msg
);
}
@override
List<Object> get props => [status, phoneNum, msg];
}
Equatable 是一个方便比较对象的库,FormzStatus 是一个判断变量是否有效的库。这两个可以不必过度关注。BLOC github 示例中有用到,我也便用了起来。
着两个类也就是一个自己实现的普通类。LoginState中有status变量,但并不只是改变这个变量LoginState的状态就会改变,其实改变其中任意一个变量,BLOC都会认为是状态改变了。
登录页面代码
class LoginPage extends StatelessWidget {
LoginPage({Key? key}) : super(key: key);
final TextEditingController _controller = TextEditingController();
// 按钮样式
final ButtonStyle _style = ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))
),
backgroundColor: MaterialStateProperty.all(Colors.redAccent),
foregroundColor: MaterialStateProperty.all(Colors.white)
);
@override
Widget build(BuildContext context) {
// 输入框文本提醒及边框颜色设置
InputDecoration _decoration = InputDecoration(
hintText: S.of(context).printPhoneNum,
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.black38)),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.black38)),
);
return BlocProvider(
create: (context) {
return LoginBloc(context);
},
child: BlocListener<LoginBloc, LoginState>(
listener: (event, state) {
if (state.status != FormzStatus.submissionSuccess ) {
Fluttertoast.showToast(
msg: state.msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black54,
textColor: Colors.white,
fontSize: 16.0
);
} else {
Fluttertoast.showToast(
msg: state.msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black54,
textColor: Colors.white,
fontSize: 16.0
);
}
},
child: Container(
color: Colors.white,
child: SafeArea(
child: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 10, top: 20),
child: Image.asset("assets/graphics/nav_back_icon.png", width: 20),
),
Padding(
padding: const EdgeInsets.only(left: 20, top: 20),
child: MyText(S.of(context).welcome, fontSize: 28,),
),
Padding(
padding: const EdgeInsets.only(left: 30, top:25, right: 30),
child: TextField(keyboardType: TextInputType.number, controller: _controller,
onChanged: (v) => _splitPhoneNumber(v), decoration: _decoration,
inputFormatters:[LengthLimitingTextInputFormatter(13)]
)
),
BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
return _nextButton(context, state);
},
),
],
),
),
),
),
)
);
}
// 手机号按 3 4 4 格式输入
int inputLength = 0;
void _splitPhoneNumber(String text) {
if (text.length > inputLength) {
//输入
if (text.length == 4 || text.length == 9) {
text = text.substring(0, text.length - 1) + " " + text.substring(text.length - 1, text.length);
_controller.text = text;
_controller.selection = TextSelection.fromPosition(TextPosition(affinity: TextAffinity.downstream, offset: text.length)); //光标移到最后
}
} else {
//删除
if (text.length == 4 || text.length == 9) {
text = text.substring(0, text.length - 1);
_controller.text = text;
_controller.selection = TextSelection.fromPosition(TextPosition(affinity: TextAffinity.downstream, offset: text.length)); //光标移到最后
}
}
inputLength = text.length;
}
// 下一步按钮
Widget _nextButton(BuildContext context, LoginState state) {
return Padding(
padding: EdgeInsets.only(top: 30, left: 35, right: 35),
child: OutlinedButton(
onPressed: () {
context.read<LoginBloc>().add(LoginSubmitted(PhoneNum.dirty(clearSpace(_controller.text))));
},
style: _style,
child: Padding(
padding: EdgeInsets.only(left: 105, right: 105),
child: MyText.color(S.of(context).next, color: Colors.white),
),
),
);
}
}
这是登录页面的实现,我们来看下涉及到BLOC的关键代码。
return BlocProvider(
create: (context) {
return LoginBloc(context);
},
child: BlocListener<LoginBloc, LoginState>(
listener: (event, state) {
if (state.status != FormzStatus.submissionSuccess ) {
Fluttertoast.showToast(
msg: state.msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black54,
textColor: Colors.white,
fontSize: 16.0
);
} else {
Fluttertoast.showToast(
msg: state.msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black54,
textColor: Colors.white,
fontSize: 16.0
);
}
},
最外层为BlocProvider
, create
参数提供LoginBloc处理类,child
为子widget
官网原文:
BlocProvider is a Flutter widget which provides a bloc to its children via BlocProvider.of<T>(context)
简单翻译下:
BlocProvider就是为子widget提供Bolc的,子widget可以通过BlocProvider.of<T>(context)
获取Bloc.
而我是跟着github示例用context.read<LoginBloc>().add()
来获取的bloc.
再看下BlocListener<LoginBloc, LoginState>
,参数listen
是参数为泛型类型的event和state,child
为子widget.
官网原文:
BlocListener is a Flutter widget which takes a BlocWidgetListener
and an optional Bloc
and invokes the listener
in response to state changes in the bloc. It should be used for functionality that needs to occur once per state change such as navigation, showing a SnackBar
, showing a Dialog
, etc...
简单翻译下:
BlocListener是一个有BlocWidgetListener
和可选的Bloc
再加上个响应bloc改变状态时会被调用的listener
的flutter组件.主要是被用在状态改变时需要提示的功能,像是显示个Snackbar
和Dialog
。
总结下就是:这玩意就是用来在状态State变更时来显示一些提示功能的,像Dialog,Toast,Sanckbar这些。
再看下这段代码。
再复制过来不好画重点,我就贴图了。
BlocBuild<LoginBloc, LoginState>
,参数builder也是参数与泛型类型一样的event和state的方法,返回一个widget。
官网原文:
BlocBuilder is a Flutter widget which requires a Bloc
and a builder
function. BlocBuilder
handles building the widget in response to new states.
简单翻译下:
BlocBuilder
是一个需要Bloc和一个builder
方法的flutter组件。BlocBuilder
是处理响应新状态时需要构建的组件。
总结下就是:如果你的组件是需要根据状态变化的,像点赞这种就用BlocBuilder
包装你的组件。
所以BlocBuilder
包装的组件越精确范围越小越好,可以避免大范围的UI刷新来提升性能。
其实我这里用的不好,因为我现在的写的这个登录demo只是一个提示吐司,UI状态并没有改变,按照官网的解释其实我不用BlocBuilder
也没问题。但是我的这个_netxtButton()
要从context中获取Bloc来发送Event.
但如果不包装BlocBuilder
的话会报错,报错提示从context中找不到Bloc,因为直接获取的context是从@override Widget build(BuildContext context)
这里获取的。
显然直接获取的context并不在BlocProvider
包装中所以获取不到Bloc
,所以我用BlocBuilder
包装了一下。或者这样处理也可以
class Demo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context){
return LoginBloc(context);
},
child: LoginPage(),
);
}
}
最后再来看下Bloc的代码,
class LoginBloc extends Bloc<LoginEvent, LoginState> implements BaseDioCallBack{
var emitter;
var phoneNum;
var TAG = "LoginBloc";
late BuildContext context;
LoginBloc(this.context) : super(const LoginState()){
on<LoginSubmitted> ( _onSubmitted );
}
_onSubmitted(LoginSubmitted submitted, Emitter<LoginState> emitter) async {
this.emitter = emitter;
phoneNum = submitted.phoneNum;
if (submitted.phoneNum.error == null && submitted.phoneNum.valid) {
Log.i(TAG, submitted.phoneNum.value);
await DioManager().get("${API.phoneVerifyCode}${submitted.phoneNum.value}", this);
} else {
emitter(state.copyWith(status: FormzStatus.invalid, phoneNum:phoneNum, msg:S.of(context).errorPhoneNum));
Log.i(TAG, "getError");
}
}
@override
void getError(String msg) {
Log.i(TAG, "getError $msg");
emitter(state.copyWith(status: FormzStatus.submissionFailure, phoneNum: phoneNum, msg: msg));
}
@override
void getSuccess(Map<String, dynamic> data) {
Log.i(TAG, "getSuccess ${data.toString()}");
var verifyCodeBean = VerifyCodeBean.fromJson(data);
emitter(state.copyWith(status: FormzStatus.submissionSuccess, phoneNum: phoneNum, msg: verifyCodeBean.msg));
}
}
说下重点,LoginBloc(this.context) : super(const LoginState())
,继承Bloc
类后要向父类传入一个默认的State,会与后面发送State比较,状态不一样才会触发UI的状态改变。on<LoginSubmitted> ( _onSubmitted );
接受的事件类型与处理方法绑定。_onSubmitted(LoginSubmitted submitted, Emitter<LoginState> emitter)
该方法必须包含着两个参数event和Emitter状态发射器,通过emitter(state)
中传入新状态发射出去。有个额外重点,如果包含耗时操作一定要用await
,async
等异步关键词去处理,否则会出问题发送不出去。 因为开启有耗时操作Bloc需要知道什么时候开始处理,什么时候能处理完成,详见 github Issues。
希望大家看到这里都能有所收获,如有问题请评论区指出我会及时改正,谢谢!