Práctica de gestión de estado de Flutter BLOC

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.

Snipaste_2022-04-13_17-08-44.png

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组件.主要是被用在状态改变时需要提示的功能,像是显示个SnackbarDialog
总结下就是:这玩意就是用来在状态State变更时来显示一些提示功能的,像Dialog,Toast,Sanckbar这些。

再看下这段代码。

Snipaste_2022-04-14_09-26-02.png 再复制过来不好画重点,我就贴图了。
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.

Snipaste_2022-04-14_09-41-18.png 但如果不包装BlocBuilder的话会报错,报错提示从context中找不到Bloc,因为直接获取的context是从@override Widget build(BuildContext context)这里获取的。

Snipaste_2022-04-14_09-47-01.png 显然直接获取的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 Blocclase, 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 awaity asyncesperar 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!

Supongo que te gusta

Origin juejin.im/post/7086277637902958622
Recomendado
Clasificación