Flutter数据绑定插件binder的使用
Flutter数据绑定插件binder的使用
概述
<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
需要能够递增计数器状态,我们可以这样写:
/// 一个业务逻辑组件可以通过应用 [Logic] 混入来访问有用的
/// 方法,如 `write` 和 `read`。
class CounterViewLogic with Logic {
const CounterViewLogic(this.scope);
/// 这个对象能够与其他组件交互。
@override
final Scope scope;
/// 我们可以使用 [write] 方法来更改由 [StateRef] 引用的状态,
/// 使用 [read] 方法来获取当前状态。
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) {
/// 我们可以在 [StateRef] 上调用 [watch] 扩展方法来重建
/// 小部件,当底层状态发生变化时。
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(
/// 我们可以在 [use] 扩展方法上调用以获取业务逻辑组件
/// 并调用适当的方法。
onPressed: () => context.use(counterViewLogicRef).increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
以上就是基本使用的全部内容。
中级用法
Select
状态可以是简单的类型,例如 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);
}
}
Consumer
如果你只想重新构建你的小部件树的一部分,而不创建新的小部件,你可以使用 <Consumer>
小部件。这个小部件可以接受一个可观察的对象(StateRef
或者 StateRef
的选择部分)。
class MyAppBar extends StatelessWidget {
const MyAppBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
title: Consumer(
watchable: userRef.select((user) => '${user.firstName} ${user.lastName}'),
builder: (context, String fullName, child) => Text(fullName),
),
);
}
}
LogicLoader
如果你想要从小部件侧触发逻辑的数据加载,那么 <LogicLoader>
是你需要的小部件!
为了使用它,你必须在需要加载数据的逻辑中实现 <Loadable>
接口。然后你需要覆盖 <load>
方法并在其中获取数据。
final usersRef = StateRef(const []);
final loadingRef = StateRef(false);
final usersLogicRef = LogicRef((scope) => UsersLogic(scope));
class UsersLogic with Logic implements Loadable {
const UsersLogic(this.scope);
@override
final Scope scope;
UsersRepository get _usersRepository => use(usersRepositoryRef);
@override
Future<void> load() async {
write(loadingRef, true);
final users = await _usersRepository.fetchAll();
write(usersRef, users);
write(loadingRef, false);
}
}
从小部件的一侧,你需要使用 <LogicLoader>
并提供你想要加载的逻辑引用:
class Home extends StatelessWidget {
const Home({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LogicLoader(
refs: [usersLogicRef],
child: const UsersView(),
);
}
}
你可以在一个子树中观察状态,以在数据正在获取时显示进度指示器:
class UsersView extends StatelessWidget {
const UsersView({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final loading = context.watch(loadingRef);
if (loading) {
return const CircularProgressIndicator();
}
// 当数据被获取后显示用户列表。
final users = context.watch(usersRef);
return ListView(...);
}
}
或者,你可以使用 <builder>
参数来达到相同的目的:
class Home extends StatelessWidget {
const Home({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LogicLoader(
refs: [usersLogicRef],
builder: (context, loading, child) {
if (loading) {
return const CircularProgressIndicator();
}
// 当数据被获取后显示用户列表。
final users = context.watch(usersRef);
return ListView();
},
);
}
}
Overrides
有时,你可能需要在某些条件下重置 <StateRef>
的初始状态或 <LogicRef>
的工厂方法:
- 当你想让子树在相同的引用下拥有自己的状态/逻辑时。
- 在测试时模拟值。
Reusing a reference under a different scope
假设我们要创建一个应用,用户可以创建计数器并查看所有计数器的总和:
我们可以这样做,通过有一个全局状态是一个整数列表,并且有一个业务逻辑组件用于添加计数器和递增它们:
final countersRef = StateRef(const []);
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(0, (a, b) => a + b),
));
现在,为了创建计数器视图,我们可以在这个视图的构造函数中包含一个 index
参数。这有一些缺点:
- 如果子小部件需要访问这个索引,我们需要将
index
传递给每一层小部件,直到我们的子小部件。 - 我们不能再使用
const
关键字了。
一个更好的方法是为每个计数器小部件创建一个 <BinderScope>
。我们将配置这个 <BinderScope>
以覆盖其后代的 <StateRef>
的初始状态。
任何 <StateRef>
或 <LogicRef>
都可以在 <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/main_overrides.dart]>(https://github.com/letsar/binder/blob/main/packages/binder/example/lib/main_overrides.dart) 文件中找到。
Mocking values in tests
假设你的应用中有一个 API 客户端:
final apiClientRef = LogicRef((scope) => ApiClient());
如果你想要在测试期间提供一个模拟实例,你可以这样做:
testWidgets('Test your view by mocking the api client', (tester) async {
final mockApiClient = MockApiClient();
// 构建我们的应用并触发一帧。
await tester.pumpWidget(
BinderScope(
overrides: [apiClientRef.overrideWith((scope) => mockApiClient)],
child: const MyApp(),
),
);
expect(...);
});
无论何时在你的应用中使用 <apiClientRef>
,都会使用 <MockApiClient>
实例而不是真实的实例。
高级用法
Computed
你可能会遇到这种情况,不同的小部件对派生状态感兴趣,这个派生状态是从不同状态计算出来的。在这种情况下,有一个全局定义这种派生状态的方法会很有帮助,这样你就不会在各个小部件中复制粘贴这个逻辑。<binder>
提供了一个 <Computed>
类来帮助你处理这种情况。
假设你有一个产品列表,每个产品都有一个价格,你可以根据价格范围(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);
...
// 对 `filteredProducts` 做一些操作。
}
注意:上述代码片段的全部代码可以在 <[example/main_computed.dart]>(https://github.com/letsar/binder/blob/main/packages/binder/example/lib/main_computed.dart) 文件中找到。
Observers
你可能希望在状态改变时观察并采取相应行动(例如,记录状态变化)。为此,你需要实现 <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',
);
// 指示此观察者是否处理了更改。
// 如果为真,则不会调用其他观察者。
return true;
}
...
BinderScope(
observers: [DelegatingStateObserver(onStateUpdated)],
child: const SubTree(),
);
Undo/Redo
<binder>
提供了一种内置的方式来移动状态更改的时间线。要在逻辑组件中调用 <undo>
和 <redo>
方法,你必须在你的树中添加一个 <MementoScope>
。<MementoScope>
能够观察它下面的所有更改:
return MementoScope(
child: Builder(builder: (context) {
return MaterialApp(
home: const MyHomePage(),
);
}),
);
然后,在存储在 <MementoScope>
下面的业务逻辑中,你将能够调用 <undo>
和 <redo>
方法。
注意:如果你不在调用 <undo>
和 <redo>
的业务逻辑上方提供 <MementoScope>
,则会在运行时抛出 AssertionError
。
Disposable
在某些情况下,你希望在 <BinderScope>
主持的业务逻辑组件被销毁之前执行某些操作。为了让这一点发生,你的逻辑需要实现 <Disposable>
接口。
class MyLogic with Logic implements Disposable {
void dispose(){
// 在逻辑消失前做一些事情。
}
}
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,
);
更多关于Flutter数据绑定插件binder的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter数据绑定插件binder的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
binder
是一个轻量级的状态管理库,专为 Flutter 应用设计。它提供了一种简单而强大的方式来管理应用状态,并实现数据绑定。binder
的核心思想是通过 Ref
和 StateRef
来管理状态,并使用 Watch
和 Use
来获取和监听状态的变化。
安装 binder
首先,你需要在 pubspec.yaml
文件中添加 binder
依赖:
dependencies:
flutter:
sdk: flutter
binder: ^0.8.0
然后运行 flutter pub get
来安装依赖。
基本使用
1. 定义状态
你可以通过 StateRef
来定义一个状态。StateRef
是一个泛型类,它指定了状态的类型。
import 'package:binder/binder.dart';
final counterRef = StateRef(0);
2. 创建 BinderScope
BinderScope
是一个包裹你的应用的组件,它提供了一个作用域,用于管理状态。
import 'package:flutter/material.dart';
import 'package:binder/binder.dart';
void main() {
runApp(
BinderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Binder Example',
home: CounterScreen(),
);
}
}
3. 使用状态
你可以使用 Watch
来监听状态的变化,并在状态变化时自动重建组件。
class CounterScreen extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
final counter = context.watch(counterRef);
return Scaffold(
appBar: AppBar(
title: Text('Counter'),
),
body: Center(
child: Text(
'Count: $counter',
style: Theme.of(context).textTheme.headline4,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.use(counterRef).state++;
},
child: Icon(Icons.add),
),
);
}
}
4. 使用 Use 来更新状态
你可以使用 Use
来获取状态并更新它。在上面的例子中,FloatingActionButton
的 onPressed
回调中使用了 context.use(counterRef).state++
来增加计数器的值。
高级用法
使用 LogicRef
和 Logic
binder
还支持使用 LogicRef
和 Logic
来封装业务逻辑。Logic
是一个类,它包含了与状态相关的逻辑。
final counterLogicRef = LogicRef((scope) => CounterLogic(scope));
class CounterLogic with Logic {
CounterLogic(this.scope);
final Scope scope;
void increment() {
final counter = read(counterRef);
write(counterRef, counter + 1);
}
}
然后你可以在组件中使用 use
来调用逻辑方法:
FloatingActionButton(
onPressed: () {
context.use(counterLogicRef).increment();
},
child: Icon(Icons.add),
);
使用 Watcher
监听多个状态
你还可以使用 Watcher
来同时监听多个状态,并在它们发生变化时执行操作。
class CounterScreen extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return Watcher((context) {
final counter = context.watch(counterRef);
// 可以在这里监听其他状态
return Scaffold(
appBar: AppBar(
title: Text('Counter'),
),
body: Center(
child: Text(
'Count: $counter',
style: Theme.of(context).textTheme.headline4,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.use(counterLogicRef).increment();
},
child: Icon(Icons.add),
),
);
});
}
}