La administración de estado siempre ha sido un enfoque en el desarrollo de Flutter, y actualizar el estado solo a través del método setState() será difícil de mantener bajo una lógica comercial compleja. En la actualidad, existen principalmente tres bibliotecas de administración de estado para administrar mejor el estado de la interfaz de usuario en el proyecto flutter, Provider , GetX , BLOC . En comparación con otros marcos, la lógica de administración de estado de BLOC es muy clara, pero para los principiantes, el costo de aprendizaje es relativamente alto y es más difícil de aprender. GetX y Provider son relativamente simples y fáciles de usar. Espero que este blog pueda ayudar a los amigos que aprenden BLOC y progresan juntos.
La gestión de estado de BLOC se gestiona en función del evento Evento y el estado Estado.
En el proceso de gestión de estado de BLOC, es necesario definir primero el evento Evento y el estado Estado.
Definición de evento
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];
}
复制代码
Definición de estado
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 es una biblioteca para comparar objetos fácilmente y FormzStatus es una biblioteca para juzgar si una variable es válida. Estos dos no pueden estar demasiado preocupados. Es útil en el ejemplo de BLOC github, y también lo uso.
Las dos clases son una clase común que se implementa a sí misma. Hay una variable de estado en LoginState, pero no solo cambiando el estado de esta variable LoginState cambiará, de hecho, cambiando cualquiera de estas variables, BLOC pensará que el estado ha cambiado.
código de la página de inicio de sesión
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),
),
),
);
}
}
复制代码
Esta es la implementación de la página de inicio de sesión, echemos un vistazo al código clave involucrado en 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));
}
}
复制代码
Hablemos del punto clave, después de LoginBloc(this.context) : super(const LoginState())
heredar la Bloc
clase, debe pasar un estado predeterminado a la clase principal, que se comparará con el estado enviado más tarde, y el cambio de estado de la interfaz de usuario se activará si el estado es diferente. on<LoginSubmitted> ( _onSubmitted );
El tipo de evento aceptado está vinculado al método del controlador. _onSubmitted(LoginSubmitted submitted, Emitter<LoginState> emitter)
El método debe contener dos parámetros event y Emitter state emitter, que se emite al emitter(state)
pasar el nuevo estado. Hay un punto adicional: si contiene operaciones que consumen mucho tiempo, debe usarse await
y async
esperar a que se procesen las palabras clave asincrónicas, de lo contrario, habrá problemas y no se podrá enviar. Debido a que la apertura de la operación Bloc, que requiere mucho tiempo, necesita saber cuándo comenzar a procesar y cuándo completar el procesamiento, consulte Problemas de github para obtener más información .
Espero que todos puedan ganar algo al ver esto. Si tiene alguna pregunta, indíquela en el área de comentarios y la corregiré a tiempo, ¡gracias!