名称: asyncredux-connector-pattern 描述: 实现连接器模式以分离智能和愚蠢小部件。覆盖创建StoreConnector小部件、实现VmFactory和Vm类、构建视图模型以及通过视图模型等式优化重建。
概述
连接器模式将存储访问逻辑与UI呈现分离。小部件不直接通过context.state和context.dispatch()访问存储,而是通过“智能”连接器小部件提取存储数据,并通过构造函数参数传递给“愚蠢”表示小部件。
为什么使用连接器模式?
- 测试简化 - 无需创建Redux存储即可测试UI小部件,通过传递模拟数据
- 关注点分离 - UI小部件专注于外观;连接器处理业务逻辑
- 可重用性 - 表示小部件独立于Redux工作
- 代码清晰度 - 小部件代码不会因状态访问和转换逻辑而杂乱
- 优化重建 - 仅在视图模型更改时重建
三个组件
1. 视图模型 (Vm)
仅包含UI小部件所需的数据。扩展Vm并列出相等字段:
class CounterViewModel extends Vm {
final int counter;
final String description;
final VoidCallback onIncrement;
CounterViewModel({
required this.counter,
required this.description,
required this.onIncrement,
}) : super(equals: [counter, description]);
}
equals列表告诉AsyncRedux在决定是否重建时比较哪些字段。回调(如onIncrement)不应包含在equals中。
2. VmFactory
将存储状态转换为视图模型。扩展VmFactory并实现fromStore():
class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
CounterFactory(connector) : super(connector);
@override
CounterViewModel fromStore() => CounterViewModel(
counter: state.counter,
description: state.description,
onIncrement: () => dispatch(IncrementAction()),
);
}
工厂可以访问:
state- 工厂创建时的存储状态dispatch()- 从回调中调度操作dispatchSync()- 用于同步调度connector- 父连接器小部件的引用
3. StoreConnector
桥接存储和UI小部件:
class CounterConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, CounterViewModel>(
vm: () => CounterFactory(this),
builder: (BuildContext context, CounterViewModel vm) => CounterWidget(
counter: vm.counter,
description: vm.description,
onIncrement: vm.onIncrement,
),
);
}
}
“愚蠢”小部件通过构造函数参数接收数据:
class CounterWidget extends StatelessWidget {
final int counter;
final String description;
final VoidCallback onIncrement;
const CounterWidget({
required this.counter,
required this.description,
required this.onIncrement,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$counter'),
Text(description),
ElevatedButton(
onPressed: onIncrement,
child: Text('Increment'),
),
],
);
}
}
重建优化
每次操作更改存储状态时,StoreConnector比较新视图模型与先前视图模型。仅当它们不同时重建(基于equals列表)。
为了防止状态更改时重建,使用notify: false:
dispatch(MyAction(), notify: false);
高级工厂技术
访问连接器属性
从连接器小部件传递数据到工厂:
class UserConnector extends StatelessWidget {
final int userId;
const UserConnector({required this.userId});
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, UserViewModel>(
vm: () => UserFactory(this),
builder: (context, vm) => UserWidget(user: vm.user),
);
}
}
class UserFactory extends VmFactory<AppState, UserConnector, UserViewModel> {
UserFactory(connector) : super(connector);
@override
UserViewModel fromStore() => UserViewModel(
// 在此访问connector.userId
user: state.users.firstWhere((u) => u.id == connector.userId),
);
}
state vs currentState()
在工厂内部:
state- 工厂创建时的状态(最终,不会更改)currentState()- 调用时的当前存储状态
这些通常匹配,但在dispatchSync()后的回调中可能不同:
@override
UserViewModel fromStore() => UserViewModel(
onSave: () {
dispatchSync(SaveAction());
// state仍有旧值
// currentState()有SaveAction后的新值
},
);
在回调中使用vm Getter
在回调中访问已计算的视图模型字段以避免冗余计算:
@override
UserViewModel fromStore() => UserViewModel(
name: state.user.name,
onSave: () {
// 使用vm.name而非从state重新计算
print('Saving user: ${vm.name}');
dispatch(SaveAction(vm.name));
},
);
注意: vm getter仅在fromStore()完成后可用。在回调中使用,而不是在视图模型构造期间。
基础工厂模式
创建基础工厂以减少样板代码:
abstract class BaseFactory<T extends StatelessWidget, Model extends Vm>
extends VmFactory<AppState, T, Model> {
BaseFactory(T connector) : super(connector);
// 常见getter
User get user => state.user;
Settings get settings => state.settings;
}
class MyFactory extends BaseFactory<MyConnector, MyViewModel> {
MyFactory(connector) : super(connector);
@override
MyViewModel fromStore() => MyViewModel(
user: user, // 使用继承的getter
);
}
可空视图模型
当无法生成有效视图模型时(例如,数据仍在加载),返回null:
class HomeConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, HomeViewModel?>( // 可空类型
vm: () => HomeFactory(this),
builder: (BuildContext context, HomeViewModel? vm) { // 可空参数
return (vm == null)
? Text("用户未登录")
: HomePage(user: vm.user);
},
);
}
}
class HomeFactory extends VmFactory<AppState, HomeConnector, HomeViewModel?> {
HomeFactory(connector) : super(connector);
@override
HomeViewModel? fromStore() { // 可空返回
return (state.user == null)
? null
: HomeViewModel(user: state.user!);
}
}
从flutter_redux迁移
如果从flutter_redux迁移,可以使用converter参数代替vm:
class MyConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, ViewModel>(
converter: (store) => ViewModel.fromStore(store),
builder: (context, vm) => MyWidget(name: vm.name),
);
}
}
class ViewModel extends Vm {
final String name;
final VoidCallback onSave;
ViewModel({required this.name, required this.onSave})
: super(equals: [name]);
static ViewModel fromStore(Store<AppState> store) {
return ViewModel(
name: store.state.name,
onSave: () => store.dispatch(SaveAction()),
);
}
}
注意:vm和converter互斥。对于新代码,推荐vm方法。
调试重建
要观察连接器何时重建,将modelObserver传递给存储:
var store = Store<AppState>(
initialState: AppState.initialState(),
modelObserver: DefaultModelObserver(),
);
在StoreConnector中添加debug: this以在日志中显示连接器类型名称:
StoreConnector<AppState, ViewModel>(
debug: this,
vm: () => Factory(this),
builder: (context, vm) => MyWidget(vm: vm),
);
覆盖toString()在视图模型中以自定义诊断输出:
class MyViewModel extends Vm {
final int counter;
MyViewModel({required this.counter}) : super(equals: [counter]);
@override
String toString() => 'MyViewModel{counter: $counter}';
}
控制台输出显示重建信息:
Model D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{counter: 5}
测试视图模型
使用Vm.createFrom()在隔离环境中测试视图模型:
test('view-model properties', () {
var store = Store<AppState>(initialState: AppState(name: "Mary"));
var vm = Vm.createFrom(store, MyFactory());
expect(vm.name, "Mary");
});
test('view-model callbacks dispatch actions', () async {
var store = Store<AppState>(initialState: AppState(name: "Mary"));
var vm = Vm.createFrom(store, MyFactory());
vm.onChangeName("Bill");
await store.waitActionType(ChangeNameAction);
expect(store.state.name, "Bill");
});
重要: Vm.createFrom()每个工厂实例只能调用一次。为每个测试创建新工厂。
完整示例
// 状态
class AppState {
final int counter;
final String description;
AppState({required this.counter, required this.description});
AppState copy({int? counter, String? description}) => AppState(
counter: counter ?? this.counter,
description: description ?? this.description,
);
}
// 操作
class IncrementAction extends ReduxAction<AppState> {
@override
AppState reduce() => state.copy(counter: state.counter + 1);
}
// 视图模型
class CounterViewModel extends Vm {
final int counter;
final String description;
final VoidCallback onIncrement;
CounterViewModel({
required this.counter,
required this.description,
required this.onIncrement,
}) : super(equals: [counter, description]);
}
// 工厂
class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
CounterFactory(connector) : super(connector);
@override
CounterViewModel fromStore() => CounterViewModel(
counter: state.counter,
description: state.description,
onIncrement: () => dispatch(IncrementAction()),
);
}
// 连接器(智能小部件)
class CounterConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, CounterViewModel>(
vm: () => CounterFactory(this),
builder: (context, vm) => CounterWidget(
counter: vm.counter,
description: vm.description,
onIncrement: vm.onIncrement,
),
);
}
}
// 表示小部件(愚蠢小部件)
class CounterWidget extends StatelessWidget {
final int counter;
final String description;
final VoidCallback onIncrement;
const CounterWidget({
required this.counter,
required this.description,
required this.onIncrement,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$counter', style: TextStyle(fontSize: 48)),
Text(description),
SizedBox(height: 20),
ElevatedButton(
onPressed: onIncrement,
child: Text('Increment'),
),
],
);
}
}
参考资料
文档中的URL:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/connector/connector-pattern
- https://asyncredux.com/flutter/connector/store-connector
- https://asyncredux.com/flutter/connector/advanced-view-model
- https://asyncredux.com/flutter/connector/cannot-generate-view-model
- https://asyncredux.com/flutter/connector/migrating-from-flutter-redux
- https://asyncredux.com/flutter/testing/testing-the-view-model
- https://asyncredux.com/flutter/basics/using-the-store-state
- https://asyncredux.com/flutter/miscellaneous/observing-rebuilds