Pratique de gestion d'état Flutter BLOC

La gestion de l'état a toujours été au centre du développement de Flutter, et la mise à jour de l'état uniquement via la méthode setState() deviendra difficile à maintenir dans une logique métier complexe. À l'heure actuelle, il existe principalement trois bibliothèques de gestion d'état pour mieux gérer l'état de l'interface utilisateur sur le projet flutter, Provider , GetX , BLOC . Par rapport à d'autres frameworks, la logique de gestion d'état de BLOC est très claire, mais pour les débutants, le coût d'apprentissage est relativement important et il est plus difficile à apprendre. GetX et Provider sont relativement simples et faciles à utiliser. J'espère que ce blog pourra aider des amis qui apprennent BLOC et progresser ensemble.

La gestion de l'état de BLOC est gérée en fonction de l'événement Event et de l'état State.

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

Dans le processus de gestion d'état de BLOC, il est nécessaire de définir d'abord l'événement Event et l'état State.

Définition d'événement

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];
}
复制代码

Définition de l'état

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 est une bibliothèque pour comparer facilement des objets, et FormzStatus est une bibliothèque pour juger si une variable est valide. Ces deux-là ne peuvent pas être trop inquiets. C'est utile dans l'exemple BLOC github, et je l'utilise aussi.

Les deux classes forment une classe commune qui s'implémente elle-même. Il y a une variable d'état dans LoginState, mais non seulement changer l'état de cette variable LoginState changera, en fait, changer l'une de ces variables, BLOC pensera que l'état a changé.

code de la page de connexion

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),
        ),
      ),
    );
  }
}
复制代码

Ceci est l'implémentation de la page de connexion, regardons le code clé impliqué dans 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));
  }
}
复制代码

Parlons du point clé, après avoir LoginBloc(this.context) : super(const LoginState())hérité de la Blocclasse, il faut passer un State par défaut à la classe parent, qui sera comparé avec le State envoyé plus tard, et le changement d'état de l'UI sera déclenché si l'état est différent. on<LoginSubmitted> ( _onSubmitted );Le type d'événement accepté est lié à la méthode du gestionnaire. _onSubmitted(LoginSubmitted submitted, Emitter<LoginState> emitter)La méthode doit contenir deux paramètres event et Emitter state émetteur, qui est émis en emitter(state)passant dans le nouvel état. Il y a un point supplémentaire : s'il contient des opérations chronophages, il faut l'utiliser awaitet asyncattendre que les mots-clés asynchrones soient traités, sinon il y aura des problèmes et ne pourra pas être envoyé. Parce que l'ouverture de l'opération chronophage, Bloc a besoin de savoir quand commencer le traitement et quand terminer le traitement, voir Problèmes de github pour plus de détails .

J'espère que tout le monde pourra gagner quelque chose en voyant cela. Si vous avez des questions, veuillez les signaler dans la zone de commentaires et je les corrigerai à temps, merci !

Je suppose que tu aimes

Origine juejin.im/post/7086277637902958622
conseillé
Classement