Flutter记录自学flutter点点滴滴flutter_bloc

Flutter 学习之旅(四十八) Flutter 状态 f

2020-10-21  本文已影响0人  Tsm_2020

今天继续看flutter_bloc 官网的demo,并且利用BLoC<Event,State> 封装一个输入框的功能,
先来说一下flutter_bloc 封装能实现什么,有什么好处,

利用flutter_bloc 中的BLoC 可以将Widget 和 业务逻辑解耦,并且利用事件的改变驱动着状态的改变,从而当 BLoCBuilder 的buildWhen为空或者 buildWhen执行后为true 的时候,执行setState,重绘Widget

上面的过程就中就包含使用flutter_bloc 中BLoC的几个重要组成部分,其中包含Event 事件 ,State 状态 , Widget, 还有BLoC

下面就使用flutter_bloc 官网的demo 来引出今天我们要说的问题

我们要实现的效果


GIF 2020-10-21 13-53-17.gif

就是两个输入框,每次输入的信息改变后,检查输入信息是否正确,如果都正确则将按钮置位可以点击的状态,不正确则显示错误的的信息

既然是两个输入框,和一个按钮,那就是是三个状态,
我们先来创建状态信息

abstract class MyFormEvent extends Equatable {
  const MyFormEvent();
  @override
  List<Object> get props => [];
}
class EmailChanged extends MyFormEvent {
  const EmailChanged({@required this.email});

  final String email;

  @override
  List<Object> get props => [email];
}
class PasswordChanged extends MyFormEvent {
  const PasswordChanged({@required this.password});
  final String password;
  @override
  List<Object> get props => [password];
}
class FormSubmitted extends MyFormEvent {}

这里为了方便调用验证信息,demo中使用formz 创建实体,来验证状态

enum EmailValidationError { invalid }
class Email extends FormzInput<String, EmailValidationError> {
  const Email.pure() : super.pure('');
  const Email.dirty([String value = '']) : super.dirty(value);

  static final _emailRegex = RegExp(
    r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$',
  );

  @override
  EmailValidationError validator(String value) {
    return _emailRegex.hasMatch(value) ? null : EmailValidationError.invalid;
  }
}

enum PasswordValidationError { invalid }
class Password extends FormzInput<String, PasswordValidationError> {
  const Password.pure() : super.pure('');
  const Password.dirty([String value = '']) : super.dirty(value);

  static final _passwordRegex =
  RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$');

  @override
  PasswordValidationError validator(String value) {
    return _passwordRegex.hasMatch(value)
        ? null
        : PasswordValidationError.invalid;
  }
}

既然事件有了,我们接下来继续创建事件对应的状态,

class  MyFormState extends Equatable {
  const MyFormState({
    this.email = const Email.pure(),
    this.password = const Password.pure(),
    this.status = FormzStatus.pure,
  });

  final Email email;
  final Password password;
  final FormzStatus status;

  MyFormState copyWith({
    Email email,
    Password password,
    FormzStatus status,
  }) {
    return MyFormState(
      email: email ?? this.email,
      password: password ?? this.password,
      status: status ?? this.status,
    );
  }

  @override
  List<Object> get props => [email, password, status];
}

我们再利用BLoC来将事件和状态关联起来

class MyFormBloc extends Bloc<MyFormEvent, MyFormState> {
  MyFormBloc() : super(const MyFormState());

  @override
  void onTransition(Transition<MyFormEvent, MyFormState> transition) {
    print(transition);
    super.onTransition(transition);
  }

  @override
  Stream<Transition<MyFormEvent, MyFormState>> transformEvents(
      Stream<MyFormEvent> events,
      TransitionFunction<MyFormEvent, MyFormState> transitionFn,
      ) {
    final debounced = events
        .where((event) => event is! FormSubmitted)
        .debounceTime(const Duration(milliseconds: 300));
    return events
        .where((event) => event is FormSubmitted)
        .mergeWith([debounced]).switchMap(transitionFn);
  }

  @override
  Stream<MyFormState> mapEventToState(MyFormEvent event) async* {
    if (event is EmailChanged) {
      final email = Email.dirty(event.email);
      yield state.copyWith(
        email: email,
        status: Formz.validate([email, state.password]),
      );
    } else if (event is PasswordChanged) {
      final password = Password.dirty(event.password);
      yield state.copyWith(
        password: password,
        status: Formz.validate([state.email, password]),
      );
    } else if (event is FormSubmitted) {
      if (state.status.isValidated) {
        yield state.copyWith(status: FormzStatus.submissionInProgress);
        await Future<void>.delayed(const Duration(seconds: 1));
        yield state.copyWith(status: FormzStatus.submissionSuccess);
      }
    }
  }
}

关于async* yield 关键字可以看上一篇文章的描述

接下来的使用方法就和我们在前面flutter_bloc 学习(一) 的使用方法差不多了,

class TsmFormValidation extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Form Validation 学习'),
        centerTitle: true,
      ),
      body: BlocProvider(
        create: (_)=>MyFormBloc(),
        child: MyForm(),
      ),
    );
  }
  
}

class MyForm extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocListener<MyFormBloc, MyFormState>(
      listener: (context, state) {
        if (state.status.isSubmissionSuccess) {
          Scaffold.of(context).hideCurrentSnackBar();
          showDialog<void>(
            context: context,
            builder: (_) => SuccessDialog(),
          );
        }
        if (state.status.isSubmissionInProgress) {
          Scaffold.of(context)
            ..hideCurrentSnackBar()
            ..showSnackBar(
              const SnackBar(content: Text('Submitting...')),
            );
        }
      },
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: <Widget>[
            EmailInput(),
            PasswordInput(),
            SubmitButton(),
          ],
        ),
      ),
    );
  }
}

class EmailInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyFormBloc, MyFormState>(
      buildWhen: (previous, current) => previous.email != current.email,
      builder: (context, state) {
        return TextFormField(
          initialValue: state.email.value,
          decoration: InputDecoration(
            icon: const Icon(Icons.email),
            labelText: 'Email',
            errorText: state.email.invalid ? 'Invalid Email' : null,
          ),
          keyboardType: TextInputType.emailAddress,
          onChanged: (value) {
            context.bloc<MyFormBloc>().add(EmailChanged(email: value));
          },
        );
      },
    );
  }
}

class PasswordInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyFormBloc, MyFormState>(
      buildWhen: (previous, current) => previous.password != current.password,
      builder: (context, state) {
        return TextFormField(
          initialValue: state.password.value,
          decoration: InputDecoration(
            icon: const Icon(Icons.lock),
            labelText: 'Password',
            errorText: state.password.invalid ? 'Invalid Password' : null,
          ),
          obscureText: true,
          onChanged: (value) {
            context.bloc<MyFormBloc>().add(PasswordChanged(password: value));
          },
        );
      },
    );
  }
}

class SubmitButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<MyFormBloc, MyFormState>(
      buildWhen: (previous, current) => previous.status != current.status,
      builder: (context, state) {
        return RaisedButton(
          onPressed: state.status.isValidated
              ? () => context.bloc<MyFormBloc>().add(FormSubmitted())
              : null,
          child: const Text('Submit'),
        );
      },
    );
  }
}

class SuccessDialog extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Dialog(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Row(
              mainAxisSize: MainAxisSize.max,
              children: <Widget>[
                const Icon(Icons.info),
                const Flexible(
                  child: Padding(
                    padding: EdgeInsets.all(10),
                    child: Text(
                      'Form Submitted Successfully!',
                      softWrap: true,
                    ),
                  ),
                ),
              ],
            ),
            RaisedButton(
              child: const Text('OK'),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        ),
      ),
    );
  }
}

demo 到这里就结束了,但是关于MyFormBloc 的实现我看了好久才明白其中的道理,
我们就以demo 发送事件, 转换事件为状态,并下发状态的这个过程来分析一下

发送事件是由context.bloc 的add 方法开始的,

context.bloc<MyFormBloc>().add(EmailChanged(email: value));

我们就从add 事件说起,首先我们先来看一下Bloc和他的父类关系,

abstract class Bloc<Event, State> extends Cubit<State>{
  final _eventController = StreamController<Event>.broadcast();
}

abstract class Cubit<State> extends Stream<State> {
  StreamController<State> _controller;
}

这里我们看到Bloc继承了Cubit并维护了一个_eventController , Cubit维护了一个_controller ,
那么在add的过程中实现了什么呢,

abstract class Bloc<Event, State> extends Cubit<State>{
  @override
  void add(Event event) {
    if (_eventController.isClosed) return;
    try {
      onEvent(event);
      _eventController.add(event);
    } on dynamic catch (error, stackTrace) {
      onError(error, stackTrace);
    }
  }
}

可以看到Bloc中重写了add 方法,并将这个事件添加到_eventController中,我们再继续看add 后,执行了什么,

abstract class Bloc<Event, State> extends Cubit<State>
    implements EventSink<Event> {
  Bloc(State initialState) : super(initialState) {
    _bindEventsToStates();
  }
  void _bindEventsToStates() {
    _transitionSubscription = transformTransitions(
      transformEvents(
        _eventController.stream,
        (event) => mapEventToState(event).map(
          (nextState) => Transition(
            currentState: state,
            event: event,
            nextState: nextState,
          ),
        ),
      ),
    ).listen(
      (transition) {
        if (transition.nextState == state && _emitted) return;
        try {
          onTransition(transition);
          emit(transition.nextState);
        } on dynamic catch (error, stackTrace) {
          onError(error, stackTrace);
        }
        _emitted = true;
      },
      onError: onError,
    );
  }
}

我们看到在bloc 的初始化方法中调用了_bindEventsToStates()这个方法,从这个方法我们可以看到这里将事件转化为状态的,这里我们预留一个疑问他是如何转化的,这个疑问是这篇文章的重点,我们暂时不去管,先将整个过程分析一下
我们可以看到_bindEventsToStates() 的listen 方法调用了emit方法,而Bloc 这个类没有重写这个方法,所以调用emit 调用的是Cubit 的emit 方法,

abstract class Cubit<State> extends Stream<State> {
  @protected
  @visibleForTesting
  void emit(State state) {
    _controller ??= StreamController<State>.broadcast();
    if (_controller.isClosed) return;
    if (state == _state && _emitted) return;
    onChange(Change<State>(currentState: this.state, nextState: state));
    _state = state;
    _controller.add(_state);
    _emitted = true;
  }
}

我们可以看到Bloc调用 super.emit 后,将这个状态添加到了_controller 中,发送出去,
这里我发现Bloc 中并没有重写listen方法,所以如果调用Bloc的listen方法就是调用Cubit 的listen方法,也就是监听的Cubit 中_controller 的stream.

看完了这个过程我们再来总结一下整个过程

Bloc 在初始化的时候创建了一个_eventController,并调用_eventController.stream.listen 方法,在调用context.bloc.add 方法的时候调用转化的方法,并且将转化后的state通过super.emit 方法 add到cubit 中_controller中,Bloc 中并没有重写listen方法,所以如果调用Bloc的listen方法就是调用Cubit 的listen方法,也就是监听的Cubit 中_controller 的stream.整个过程就完成了,

下面我们仔细来说一下_bindEventsToStates()这个方法

/// 转化Stream  ,如果你需要在创建Stream后,做一些变换的话,这是一个好的方法
  Stream<Transition<Event, State>> transformTransitions(
    Stream<Transition<Event, State>> transitions,
  ) {
    return transitions;
  }
///  重新添加Stream 
  Stream<Transition<Event, State>> transformEvents(
    Stream<Event> events,
    TransitionFunction<Event, State> transitionFn,
  ) {
    return events.asyncExpand(transitionFn);
  }

  void _bindEventsToStates() {
    _transitionSubscription = transformTransitions(
      transformEvents(
        _eventController.stream,
        (event) => mapEventToState(event).map(
          (nextState) => Transition(
            currentState: state,
            event: event,
            nextState: nextState,
          ),
        ),
      ),
    ).listen(
     ....
    );
  }

我们可以看到他就是调用了_eventController.stream.asyncExpand(Stream<E>? convert(T event))后并且监听他,
那么asyncExpand方法中到底实现了什么呢,

  Stream<E> asyncExpand<E>(Stream<E>? convert(T event)) {
    _StreamControllerBase<E> controller;
    ////先创建controller
    if (isBroadcast) {
      controller = _SyncBroadcastStreamController<E>(null, null);
    } else {
      controller = _SyncStreamController<E>(null, null, null, null);
    }
  ///监听controller
    controller.onListen = () {
///获取原有subscription 
      StreamSubscription<T> subscription = this.listen(null,
          onError: controller._addError, // Avoid Zone error replacement.
          onDone: controller.close);
///在原有的subscription收到回调的时候调用
      subscription.onData((T event) {
        Stream<E>? newStream;
        try {
///创建新的stream 
          newStream = convert(event);
        } catch (e, s) {
          controller.addError(e, s);
          return;
        }
        if (newStream != null) {
          subscription.pause();
/// 转化controller 没有发生错误,则关联新的controller和新的stream,并返回stream
          controller.addStream(newStream).whenComplete(subscription.resume);
        }
      });
      controller.onCancel = subscription.cancel;
      if (!isBroadcast) {
        controller
          ..onPause = subscription.pause
          ..onResume = subscription.resume;
      }
    };
    return controller.stream;
  }

这里我们就分析完了,根据这个asyncExpand 方法我们可以看到,每次收到信息的信息的时候都会重新创建Stream,好处是我们可以通过transformTransitions来干预生成的结果原因他是一个Stream,但是这种方法新人的话肯定肯定是想不到的,那么我怎么改才更容易让人理解呢,
看看我们修改后的代码

Transition<Event, State> mapEventToState(Event event);


  void _bindEventsToStates() {
    _eventController.stream.listen((event) {
      try {
        Transition transition = mapEventToState(event).map(
              (nextState) =>
              Transition(
                currentState: state,
                event: event,
                nextState: nextState,
              ),
        );
        printString(transition.toString());
        if (transition.nextState == state && _emitted) return;
        onTransition(transition);
        emit(transition.nextState);
        _emitted = true;
      } catch (error, stackTrace) {
        onError(error, stackTrace);
      }
    });
  }

这种方式更让人容易理解,但是对后续操作一点也不友好,

我学习flutter的整个过程都记录在里面了
https://www.jianshu.com/c/36554cb4c804

最后附上demo 地址

https://github.com/tsm19911014/tsm_flutter

上一篇 下一篇

猜你喜欢

热点阅读