Flutter数据绑定插件binder的使用

Flutter数据绑定插件binder的使用

概述

<binder> 是一个轻量且功能强大的工具,用于将你的应用状态与业务逻辑分离。它旨在简化状态管理,使开发更加高效。

视图与状态分离

在其他状态管理模式中,<binder> 的目标是将应用状态与更新它的业务逻辑分离:

Data flow

我们可以将整个应用状态视为许多微小状态的聚合。每个状态都是独立的。视图可以关注某些特定的状态,并通过逻辑组件来更新它们。

安装

在你的 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 参数可以被逻辑组件用来更改状态并访问其他逻辑组件。

注意:如果你想让 StateRefLogicRef 对象在整个应用中可访问,你可以将其声明为公共全局变量。

如果我们的 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

状态可以是简单的类型,例如 intString,但它也可以更复杂,例如:

class User {
  const User(this.firstName, this.lastName, this.score);

  final String firstName;
  final String lastName;
  final int score;
}

有些应用的视图只对全局状态的部分感兴趣。在这种情况下,选择仅关注状态的一部分可能是更有效的。

例如,如果我们有一个应用程序栏标题,它只负责显示 User 的全名,并且我们不希望每次分数变化时都重新构建它,我们将使用 StateRefselect 方法来仅观察状态的一部分:

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

假设我们要创建一个应用,用户可以创建计数器并查看所有计数器的总和:

Counters

我们可以这样做,通过有一个全局状态是一个整数列表,并且有一个业务逻辑组件用于添加计数器和递增它们:

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> 类来帮助你处理这种情况。

假设你有一个产品列表,每个产品都有一个价格,你可以根据价格范围(minPriceRefmaxPriceRef)过滤这些产品。

你可以定义以下 <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

1 回复

更多关于Flutter数据绑定插件binder的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


binder 是一个轻量级的状态管理库,专为 Flutter 应用设计。它提供了一种简单而强大的方式来管理应用状态,并实现数据绑定。binder 的核心思想是通过 RefStateRef 来管理状态,并使用 WatchUse 来获取和监听状态的变化。

安装 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 来获取状态并更新它。在上面的例子中,FloatingActionButtononPressed 回调中使用了 context.use(counterRef).state++ 来增加计数器的值。

高级用法

使用 LogicRefLogic

binder 还支持使用 LogicRefLogic 来封装业务逻辑。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),
        ),
      );
    });
  }
}
回到顶部