FlutterBLOC状態管理の実践

状態管理は常にFlutter開発の焦点であり、setState()メソッドを介してのみ状態を更新することは複雑なビジネスロジックの下で維持するのが難しくなります。現在、フラッタープロジェクトのUI状態をより適切に管理するために、主に3つの状態管理ライブラリ(ProviderGetXBLOC )があります。他のフレームワークと比較して、BLOCの状態管理ロジックは非常に明確ですが、初心者にとっては、学習コストが比較的高く、学習がより困難です。GetXとプロバイダーは比較的シンプルで使いやすいです。このブログがBLOCを学び、一緒に進歩する友人に役立つことを願っています。

BLOCの状態管理は、イベントイベントと状態状態に基づいて管理されます。

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

BLOCの状態管理プロセスでは、最初にイベントイベントと状態状態を定義する必要があります。

イベント定義

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

状態の定義

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は変数が有効かどうかを判断するためのライブラリです。これらの2つは過度に心配することはできません。これはBLOCgithubの例で役立ち、私も使用しています。

2つのクラスは、それ自体を実装する共通のクラスです。LoginStateにはステータス変数がありますが、この変数の状態を変更すると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组件.主要是被用在状态改变时需要提示的功能,像是显示个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));
  }
}
复制代码

重要な点について話しましょう。クラスをLoginBloc(this.context) : super(const LoginState())継承した後Bloc、デフォルトの状態を親クラスに渡す必要があります。これは後で送信される状態と比較され、状態が異なる場合はUIの状態変更がトリガーされます。on<LoginSubmitted> ( _onSubmitted );受け入れられたイベントタイプは、ハンドラーメソッドにバインドされます。メソッドには、2つのパラメーターeventと、新しい状態を渡すこと_onSubmitted(LoginSubmitted submitted, Emitter<LoginState> emitter)によって発行されるエミッター状態エミッターが含まれている必要があります。追加のポイントがあります。時間のかかる操作が含まれている場合は、それを使用して非同期キーワードが処理されるの待つ必要があります。そうしないと、問題が発生して送信できなくなります。時間のかかる操作を開くには、Blocが処理を開始するタイミングと処理を完了するタイミングを知る必要があるため、詳細についてはgithubの問題を参照してください。emitter(state)awaitasync

みなさんに見ていただきたいと思います。ご不明な点がございましたら、コメント欄でご指摘いただければ、時間内に修正させていただきますので、よろしくお願いいたします。

おすすめ

転載: juejin.im/post/7086277637902958622