Flutter BLOC状态管理 实践

Flutter开发中状态管理一直是一个重点,仅通过setState()方法来更新状态在复杂的业务逻辑下会变得难以维护。目前主要有三个状态管理库来更好的管理flutter项目上的UI状态,Provider, GetX, BLOC。BLOC相对于其他框架他的状态管理逻辑十分清晰,但对于初学者来说学习成本也比较大,学习起来也比较困难。GetX和Provider则是比较简单易用。希望此篇博客可以帮助到学习BLOC的朋友,一起共同进步。

BLOC的状态管理是基于事件Event和状态State来管理的。

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

在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组件.主要是被用在状态改变时需要提示的功能,像是显示个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类后要向父类传入一个默认的State,会与后面发送State比较,状态不一样才会触发UI的状态改变。on<LoginSubmitted> ( _onSubmitted );接受的事件类型与处理方法绑定。_onSubmitted(LoginSubmitted submitted, Emitter<LoginState> emitter)该方法必须包含着两个参数event和Emitter状态发射器,通过emitter(state)中传入新状态发射出去。有个额外重点,如果包含耗时操作一定要用await,async等异步关键词去处理,否则会出问题发送不出去。 因为开启有耗时操作Bloc需要知道什么时候开始处理,什么时候能处理完成,详见 github Issues

希望大家看到这里都能有所收获,如有问题请评论区指出我会及时改正,谢谢!

猜你喜欢

转载自juejin.im/post/7086277637902958622