Flutter实战-原来你是这样的Bloc

Flutter实战-原来你是这样的Bloc

如何理解这张图

image.png

在代码中体现

ui

1、监听状态,更新widget
2、功能交互,向其它widget 发送事件

BlocBuilder<LoginPageCubit, LoginPageState>(  
/// 接收通知,更新当前widget
builder: (context, state) {
return Visibility(
visible: state.phoneNum.isNotEmpty,
child: Container(
margin: EdgeInsets.only(right: 16.rdp),
alignment: Alignment.centerRight,
child: GestureDetector(
child: SvgPicture.asset(LoginImg.LOGIN_IC_CLOSE),
onTap: () {
_phoneController.clear();
/// 通知状态机更新状态
context.read<LoginPageCubit>().setPhoneNum("");
},
)),
);
},

data

维护了整个widget所需要的状态,data中数据的更新会体现到ui上,同时,ui上更新了数据也需要同步到状态机中。

1、业务利用bloc的机制实现状态机

class LoginPageCubit extends Cubit<LoginPageState> {  
LoginPageCubit() : super(LoginPageState());

/// 利用bloc的机制,通知widget更新状态
void setPhoneNum(String phoneNum) {
// 各种逻辑操作,网络请求,数据存储等等等。。。
...
// 最终得到结果,状态机通知widget进行状态更新
emit(state.copyWith(phoneNum: phoneNum));
}
}

2、业务注册状态机

return MultiBlocProvider(  
providers: [
BlocProvider(create: (context) => LoginPageCubit()),
],
child: Scaffold(resizeToAvoidBottomInset: false, body: LoginView()));

bloc

bloc提供的各种api:BlocProviderBlocBuilder、以及emit(),起到桥的作用,完成状态机和ui桥接工作。

进阶使用

在Flutter_Bloc中常用的api有这些

BlocListener

基础监听器,其它的衍生Listener都是基于这个api实现,同时,它也可以单独使用。
有的时候,我们只需要监听状态更新,不需要更新UI,例如:监听到手机号为空时,弹出一个toast。这种情况,就可以使用BlocListener实现需求

BlocListener<LoginPageCubit, LoginPageState>(  
listener: (context, state){
print("接收到状态更新,弹出toast");
},
child: Container(
margin: EdgeInsets.only(top: 2.5.rdp),
child: CustomCheckbox(
...
),
)
),

BlocSelector

data中我们创建了一个状态机,进行更新状态,状态机中的状态有很多个:手机号、验证码、隐私协议同意等等。正常情况下,只要更新了状态,所有的BlocBuilder都会更新。

是否有一种更加颗粒化的方式,更新手机号的状态,就只有手机号的widget会刷新,其它widget不需要刷新。

针对这种情况,bloc也提供了对应的api来满足更加颗粒化的刷新。
BlocSelector<B extends StateStreamable<S>, S, T>,T 为传递给build的状态

BlocSelector<LoginPageCubit, LoginPageState, bool>(  
selector: (state){
// 设置当前widget只关心的状态
return state.isAgree;
},

// LoginPageState 其它状态的改变,不会触发当前widget的更新
// 在这个widget中,只有隐私协议状态的修改才会触发更新
builder: (context, state) {
return Container(
margin: EdgeInsets.only(top: 2.5.rdp),
child: CustomCheckbox(
value: state,
onChanged: (newValue) {
context.read<LoginPageCubit>().setProtocolState(newValue);
},
size: 14.rdp,
customIcon: SvgPicture.asset(state
? CommonImg.COMMON_IC_CB_CHECKED
: CommonImg.COMMON_IC_CB_UNCHECKED),
),
);
},
),

BlocBuilder效果,我在下面的隐私协议中监听了状态机的状态,上面的手机号输入会更新状态机的状态,因此隐私协议的widget也出现了更新。
flutter_bloc_builder_20240723.gif

Blocselector效果,使用了Blocselector后,隐私协议的widget只会关心isAgree这个状态,手机号的输入并不会导致隐私协议的widget出现更新。
flutter_bloc_selector_20240723.gif

BlocConsumer

有的时候,我们需要在状态更新的时候做两件事:
1、弹出一个toast、打开一个dialog
2、并同时需要更新widget

如果使用的是BlocBuilder,看起来可以在builder中直接弹toast,eg:

builder: (context, state) { 
SmartDialog.showToast("xxxx");
return Container(
margin: EdgeInsets.only(top: 2.5.rdp),
...
)
}

看起来是没问题,但是在实际运行中会出现下面错误

[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: 'package:flutter/src/widgets/overlay.dart': Failed assertion: line 457 pos 12: '!_entries.contains(entry)': The specified entry is already present in the Overlay.

具体的原因?

这种情况下就可以使用BlocConsumer 来实现需求

BlocConsumer<LoginPageCubit, LoginPageState>(  
listener: (context, state) {
SmartDialog.showToast("xxxx");
},
builder: (context, state) {
return Container(
margin: EdgeInsets.only(top: 2.5.rdp),
....
);
},
),

MultiBlocListener

有时候,也许一个page特别复杂,需要多个状态机协作,就可以用到MultiBlocListener

源码

bloc的架构是典型的发布-订阅模式,通过Stream来实现发布-订阅功能,使用Provider实现了widget中cubit的全局共享。

Bloc模型

bloc发布订阅

Cubit

在[[#如何理解这张图]]中,首先看看Cubit是怎么创建的

class LoginPageCubit extends Cubit<LoginPageState> {  
LoginPageCubit() : super(LoginPageState());
....
}

bloc_base.dart

abstract class BlocBase<State>  
implements StateStreamableSource<State>, Emittable<State>, ErrorSink {
/// {@macro bloc_base}
BlocBase(this._state) {
// ignore: invalid_use_of_protected_member
_blocObserver.onCreate(this);
}

late final _stateController = StreamController<State>.broadcast();
}

Cubit中,创建了一个StreamController,在这里发布者和订阅是一对多的关系,因此它使用了broadcast模式。

Listener

BlocBuilder,我们是如何订阅事件的

Padding(  
child: BlocBuilder<AreaCodeCubit, AreaCodeEvent>(
builder: (context, state) {
return Text(
.....
);
},
),
),

我们在BlocBuilderbuilder构建widget,BlocBuilder使用BlocListener更新widget

@override  
Widget build(BuildContext context) {
if (widget.bloc == null) {
// Trigger a rebuild if the bloc reference has changed.
// See https://github.com/felangel/bloc/issues/2127.
context.select<B, bool>((bloc) => identical(_bloc, bloc));
}
return BlocListener<B, S>(
bloc: _bloc,
listenWhen: widget.buildWhen,
listener: (context, state) => setState(() => _state = state),
child: widget.build(context, _state),
);
}

在BlocListener中,从Context取出Cubit,进行事件订阅
bloc_listener.dart

void _subscribe() {  
// 拿到了bloc的StreamController,进行事件订阅
_subscription = _bloc.stream.listen((state) {
if (widget.listenWhen?.call(_previousState, state) ?? true) {

// 回调BlockListener的listener方法
widget.listener(context, state);
}
_previousState = state;
});
}

单监听到新状态是,BlocListener的listener中使用setState更新状态,触发builder: (context, state)的重建,实现wiget的刷新。

由于builder中的widget属于BlocBuilder,因此setState只刷新BlocBuilder中的widget,这样达到了局部刷新的效果。

Porvider

FlutterBloc还讲到了Cubit的共享,先看看Cubit是怎么注册的。

BlocProvider(create: (context) => LoginPageCubit())

flutter_bloc中,还用到了一个状态管理的库Provider
bloc_provider.dart

Widget buildWithChild(BuildContext context, Widget? child) {  
assert(
child != null,
'$runtimeType used outside of MultiBlocProvider must specify a child',
);
final value = _value;
return value != null
? InheritedProvider<T>.value(
value: value,
startListening: _startListening,
lazy: lazy,
child: child,
)
// 使用了`Provider`,将funcation传了进去
: InheritedProvider<T>(
create: _create,
dispose: (_, bloc) => bloc.close(),
startListening: _startListening,
child: child,
lazy: lazy,
);
}

inherited_provider.dart

class _CreateInheritedProviderState<T>  
extends _DelegateState<T, _CreateInheritedProvider<T>> {
VoidCallback? _removeListener;
bool _didInitValue = false;
T? _value;
_CreateInheritedProvider<T>? _previousWidget;
FlutterErrorDetails? _initError;

@override
T get value {
....
// 执行了funcation,将cubit 缓存在 provider中
_value = delegate.create!(element!);
....
}
}

Cubit已经注册到了Context,那么如何取出Cubit呢,先看看如何取出

context.read<LoginPageCubit>().setPhoneNum("");

provider.dart

T read<T>() {  
return Provider.of<T>(this, listen: false);
}

provider.dart

static T of<T>(BuildContext context, {bool listen = true}) {  
....
final inheritedElement = _inheritedElementOf<T>(context);
if (listen) {
// bind context with the element
// We have to use this method instead of dependOnInheritedElement, because
// dependOnInheritedElement does not support relocating using GlobalKey
// if no provider were found previously. context.dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>();
}

// 这个便是BlocProvider缓存的Cubit
final value = inheritedElement?.value;
if (_isSoundMode) {
if (value is! T) {
throw ProviderNullException(T, context.widget.runtimeType);
}
return value;
}
return value as T;
}

Bloc的状态管理

状态机

flutter-bloc中,有状态机的概念,ui元素和状态机一一对应,状态中值的修改可以自动映射到ui上,同样的,ui上对应元素的修改也要反馈到状态机。

bloc是一个发布-订阅的消息框架,flutter-bloc在针对flutter做了进一步的封装,更加符合数据驱动的方式。

使用BlocListener监听event,然后使用setState更新widget.build(context, _state),从而实现widget的更新。

状态管理

Flutter 常见的状态管理方式有下面几种:

  • Widget 管理自己的状态
  • Widget 管理子 Widget 状态
  • 混合管理(父 Widget 和子 Widget 都管理状态)

Bloc是一个状态管理库,可以实现上面的几种管理方式,那么Bloc是如何实现状态管理的呢?
回顾下Bloc的模型:

1、状态机注册的时候,BlocProviderBuildContext绑定在了一起。

2、Widget需要发送事件时需要从context读取provider,从而取到状态机。

context.read<LoginPageCubit>().setPhoneNum("");

3、如果需要监听事件,需要使用BlocListener监听并构建widget

children: [  
BlocSelector<LoginPageCubit, LoginPageState, bool>(
selector: (state) {
return state.isAgree;
},
builder: (context, state) {
return Container(
...
);
},
),
Expanded(child: ProtocolText())
],

4、BlocListener收到事件时,通过setState更新Widget

单元测试

bloc/packages/bloc/test at master · felangel/bloc (github.com)

group('constructor', () {  
late BlocObserver observer;
setUp(() {
// mock一个observer
observer = MockBlocObserver();
Bloc.observer = observer;
});
test('triggers onCreate on observer', () {
final cubit = CounterCubit();
// 验证oncreate 是否被调用一次
verify(() => observer.onCreate(cubit)).called(1);
});
});

group('emit', (){
test('throws StateError if cubit is closed', () {
var didThrow = false;
// runZonedGuarded 捕获并处理异常
runZonedGuarded(() {
final cubit = CounterCubit();
expectLater(
cubit.stream,
emitsInOrder(<Matcher>[equals(1), emitsDone]),
);
// 关闭stream后再调用increment,将抛出异常
cubit
..increment()
..close()
..increment();
}, (error, _) {
didThrow = true;
// 是否达到期望值
expect(
error,
isA<StateError>().having(
(e) => e.message,
'message',
'Cannot emit new states after calling close',
),
);
});
expect(didThrow, isTrue);
});

test('emits states in the correct order', () async {
final states = <int>[];
final cubit = CounterCubit();
final subscription = cubit.stream.listen(states.add);
cubit.increment();
await cubit.close();
await subscription.cancel();
expect(states, [1]);
});
}

bloc/packages/flutter_bloc/test/bloc_consumer_test.dart at master · felangel/bloc (github.com)

group('BlocConsumer', (){  
// 测试widgets
testWidgets(
'accesses the bloc directly and passes initial state to builder and '
'nothing to listener', (tester) async {
final counterCubit = CounterCubit();
final listenerStates = <int>[];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BlocConsumer<CounterCubit, int>(
bloc: counterCubit,
builder: (context, state) {
return Text('State: $state');
},
listener: (_, state) {
listenerStates.add(state);
},
),
),
),
);
// 查找'State: 0' 并判断该配置是否存在
expect(find.text('State: 0'), findsOneWidget);
expect(listenerStates, isEmpty);
});

testWidgets(
'accesses the bloc directly '
'and passes multiple states to builder and listener', (
tester) async {
final counterCubit = CounterCubit();
final listenerStates = <int>[];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BlocConsumer<CounterCubit, int>(
bloc: counterCubit,
builder: (context, state) {
return Text('State: $state');
},
listener: (_, state) {
listenerStates.add(state);
},
),
),
),
);
expect(find.text('State: 0'), findsOneWidget);
expect(listenerStates, isEmpty);
counterCubit.increment();
await tester.pump();
// 验证widget是否完成了修改
expect(find.text('State: 1'), findsOneWidget);
expect(listenerStates, [1]);
});
}
   Vector Landscape Vectors by Vecteezy

Flutter实战-原来你是这样的Bloc

https://www.laoyuyu.me/2024/08/11/flutter/flutter_block/

作者

AriaLyy

发布于

2024-08-11

许可协议

CC BY-NC-SA 4.0

评论