binder
一种轻量级但功能强大的方式,可将您的应用程序状态与业务逻辑绑定。
愿景
与其他状态管理模式一样,binder 旨在将应用程序状态与更新它的业务逻辑分离开来。

我们可以将整个应用程序状态看作是许多微小状态的集合。每个状态都独立于其他状态。
视图可能只关注某些特定状态,并且必须使用逻辑组件来更新它们。
入门
安装
在您的 flutter 项目的 pubspec.yaml 文件中,添加以下依赖项
dependencies:
binder: <latest_version>
在您的库中添加以下导入
import 'package:binder/binder.dart';
基本用法
任何状态都必须通过 StateRef 及其初始值来声明。
final counterRef = StateRef(0);
注意:状态应该是不可变的,因此更新它的唯一方法是通过此包提供的函数。
任何逻辑组件都必须通过 LogicRef 及其用于创建它的函数来声明。
final counterViewLogicRef = LogicRef((scope) => CounterViewLogic(scope));
scope 参数随后可用于逻辑来修改状态和访问其他逻辑组件。
注意:您可以将 StateRef 和 LogicRef 对象声明为公共全局变量,如果您希望它们能从您应用的其余部分访问的话。
如果我们希望我们的 CounterViewLogic 能够递增我们的计数器状态,我们可能会这样写:
/// A business logic component can apply the [Logic] mixin to have access to
/// useful methods, such as `write` and `read`.
class CounterViewLogic with Logic {
const CounterViewLogic(this.scope);
/// This is the object which is able to interact with other components.
@override
final Scope scope;
/// We can use the [write] method to mutate the state referenced by a
/// [StateRef] and [read] to obtain its current state.
void increment() => write(counterRef, read(counterRef) + 1);
}
为了将所有这些内容绑定到 Flutter 应用中,我们必须使用一个名为 BinderScope 的专用小部件。
该小部件负责保存应用程序状态的一部分并提供逻辑组件。
您通常会在 MaterialApp 小部件之上创建此小部件。
BinderScope(
child: MaterialApp(
home: CounterView(),
),
);
在 BinderScope 下的任何小部件中,您都可以调用 BuildContext 上的扩展方法,将视图绑定到应用程序状态和业务逻辑组件。
class CounterView extends StatelessWidget {
const CounterView({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// We call the [watch] extension method on a [StateRef] to rebuild the
/// widget when the underlaying state changes.
final counter = context.watch(counterRef);
return Scaffold(
appBar: AppBar(title: const Text('Binder example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text('$counter', style: Theme.of(context).textTheme.headline4),
],
),
),
floatingActionButton: FloatingActionButton(
/// We call the [use] extension method to get a business logic component
/// and call the appropriate method.
onPressed: () => context.use(counterViewLogicRef).increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
以上是您进行基本使用所需了解的所有内容。
注意:上述代码段的全部代码可在 [example][example_main] 文件中找到。
中级用法
选择
状态可以是简单的类型,如 int 或 String,但也可以更复杂,例如:
class User {
const User(this.firstName, this.lastName, this.score);
final String firstName;
final String lastName;
final int score;
}
应用程序的某些视图只对全局状态的某些部分感兴趣。在这些情况下,只选择对这些视图有用的状态部分可能更有效。
例如,如果我们有一个应用程序栏标题,它只负责显示 User 的全名,并且我们不希望它在分数每次发生变化时都重建,我们将使用 StateRef 的 select 方法来仅监视状态的子部分。
class AppBarTitle extends StatelessWidget {
const AppBarTitle({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final fullName = context.watch(
userRef.select((user) => '${user.firstName} ${user.lastName}'),
);
return Text(fullName);
}
}
覆盖
在某些情况下,能够覆盖 StateRef 的初始状态或 LogicRef 的工厂可能很有用。
- 当我们希望某个子树在同一引用下拥有自己的状态/逻辑时。
- 用于在测试中模拟值。
在不同的作用域下重用引用。
假设我们要创建一个应用程序,用户可以创建计数器并查看所有计数器的总和。

我们可以通过让全局状态成为一个整数列表,并有一个用于添加计数器和递增它们的业务逻辑组件来实现。
final countersRef = StateRef(const <int>[]);
final countersLogic = LogicRef((scope) => CountersLogic(scope));
class CountersLogic with Logic {
const CountersLogic(this.scope);
@override
final Scope scope;
void addCounter() {
write(countersRef, read(countersRef).toList()..add(0));
}
void increment(int index) {
final counters = read(countersRef).toList();
counters[index]++;
write(countersRef, counters);
}
}
然后,我们可以在小部件中使用 select 扩展方法来监视此列表的总和。
final sum = context.watch(countersRef.select(
(counters) => counters.fold<int>(0, (a, b) => a + b),
));
现在,为了创建计数器视图,我们可以在此视图的构造函数中包含一个 index 参数。
这有一些缺点
- 如果子小部件需要访问此索引,我们将需要将
index传递给树中的每个小部件,直到我们的子小部件。 - 我们不能再使用
const关键字了。
更好的方法是在每个计数器小部件之上创建 BinderScope。然后,我们将配置此 BinderScope 来覆盖其后代的状态 StateRef,并提供不同的初始值。
任何 StateRef 或 LogicRef 都可以在 BinderScope 中被覆盖。在查找当前状态时,后代将获取 BinderScope 中第一个被覆盖的引用的状态,直到根 BinderScope。
可以这样写:
final indexRef = StateRef(0);
class HomeView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final countersCount =
context.watch(countersRef.select((counters) => counters.length));
return Scaffold(
...
child: GridView(
...
children: [
for (int i = 0; i < countersCount; i++)
BinderScope(
overrides: [indexRef.overrideWith(i)],
child: const CounterView(),
),
],
),
...
);
}
}
BinderScope 构造函数有一个 overrides 参数,可以从 StateRef 和 LogicRef 实例上的 overrideWith 方法提供。
注意:上述代码段的全部代码可在 [example][example_main_overrides] 文件中找到。
模拟测试中的值
假设您的应用程序中有一个 API 客户端。
final apiClientRef = LogicRef((scope) => ApiClient());
如果您想在测试期间提供一个模拟的,您可以这样做:
testWidgets('Test your view by mocking the api client', (tester) async {
final mockApiClient = MockApiClient();
// Build our app and trigger a frame.
await tester.pumpWidget(
BinderScope(
overrides: [apiClientRef.overrideWith((scope) => mockApiClient)],
child: const MyApp(),
),
);
expect(...);
});
每当您的应用程序中使用 apiClientRef 时,都会使用 MockApiClient 实例而不是实际实例。
高级用法
计算值
您可能会遇到这样的情况:不同的视图对由不同状态计算得出的派生状态感兴趣。在这种情况下,能够全局定义此派生状态可能会有所帮助,这样您就不必在视图之间复制代码/粘贴此逻辑。
Binder 提供了一个 Computed 类来帮助您处理这种情况。
假设您有一个由 productsRef 引用的产品列表,每个产品都有价格,您可以根据价格范围(由 minPriceRef 和 maxPriceRef 引用)过滤这些产品。
然后,您可以定义以下 Computed 实例:
final filteredProductsRef = Computed((watch) {
final products = watch(productsRef);
final minPrice = watch(minPriceRef);
final maxPrice = watch(maxPriceRef);
return products
.where((p) => p.price >= minPrice && p.price <= maxPrice)
.toList();
});
像 StateRef 一样,您可以在小部件的构建方法中监视 Computed。
@override
Widget build(BuildContext context) {
final filteredProducts = context.watch(filteredProductsRef);
...
// Do something with `filteredProducts`.
}
注意:上述代码段的全部代码可在 [example][example_main_computed] 文件中找到。
观察者
您可能希望在状态更改时进行观察并采取相应操作(例如,记录状态更改)。
为此,您需要实现 StateObserver 接口(或使用 DelegatingStateObserver),并将实例提供给 BinderScope 构造函数的 observers 参数。
bool onStateUpdated<T>(StateRef<T> ref, T oldState, T newState, Object action) {
logs.add(
'[${ref.key.name}#$action] changed from $oldState to $newState',
);
// Indicates whether this observer handled the changes.
// If true, then other observers are not called.
return true;
}
...
BinderScope(
observers: [DelegatingStateObserver(onStateUpdated)],
child: const SubTree(),
);
撤销/重做
Binder 提供了一种内置方法来在状态更改的时间线上移动。
要能够撤销/重做状态更改,您必须在树中添加一个 MementoScope。MementoScope 将能够观察它下面的所有更改。
return MementoScope(
child: Builder(builder: (context) {
return MaterialApp(
home: const MyHomePage(),
);
}),
);
然后,在位于 MementoScope 下方的业务逻辑中,您将能够调用 undo/redo 方法。
注意:如果您未在调用 undo/redo 的业务逻辑上方提供 MementoScope,您将在运行时收到一个 AssertionError。
可 Dispose
在某些情况下,您可能希望在托管业务逻辑组件的 BinderScope 被 Dispose 之前执行某些操作。要有机会做到这一点,您的逻辑需要实现 Disposable 接口。
class MyLogic with Logic implements Disposable {
void dispose(){
// Do some stuff before this logic go away.
}
}
StateListener
如果您想导航到另一个屏幕或在状态更改时显示对话框,您可以使用 StateListener 小部件。
例如,在身份验证视图中,您可能希望在身份验证失败时显示一个警告对话框。
为此,在逻辑组件中,您可以设置一个指示身份验证是否成功的状态,并在视图中有一个 StateListener 来响应这些状态更改。
return StateListener(
watchable: authenticationResultRef,
onStateChanged: (context, AuthenticationResult state) {
if (state is AuthenticationFailure) {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Error'),
content: const Text('Authentication failed'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Ok'),
),
],
);
},
);
} else {
Navigator.of(context).pushReplacementNamed(route_names.home);
}
},
child: child,
);
在上面的代码片段中,每当由 authenticationResultRef 引用的状态发生更改时,都会触发 onStateChanged 回调。在此回调中,我们仅检查状态的类型以确定是否需要显示警告对话框。
DartDev Tools
Binder 旨在简化您的应用程序调试。通过使用 DartDev tools,您将能够检查由任何 BinderScope 托管的当前状态。