Creator 是一个状态管理库,可以使业务逻辑代码简洁、流畅、可读且可测试

以编译时安全的方式读取和更新状态

// Creator creates a stream of data.
final counter = Creator.value(0);

Widget build(BuildContext context) {
  return Column(
    children: [
      // Watcher will rebuild whenever counter changes.
      Watcher((context, ref, _) => Text('${ref.watch(counter)}')),
      TextButton(
        // Update state is easy.
        onPressed: () => context.ref.update<int>(counter, (count) => count + 1),
        child: const Text('+1'),
      ),
    ],
  );
}

编写清晰且可测试的业务逻辑

// repo.dart

// Pretend calling a backend service to get fahrenheit temperature.
Future<int> getFahrenheit(String city) async {
  await Future.delayed(const Duration(milliseconds: 100));
  return 60 + city.hashCode % 20;
}

// logic.dart

// Simple creators bind to UI.
final cityCreator = Creator.value('London');
final unitCreator = Creator.value('Fahrenheit');

// Write fluid code with methods like map, where, etc.
final fahrenheitCreator = cityCreator.mapAsync(getFahrenheit);

// Combine creators for business logic. 
final temperatureCreator = Emitter<String>((ref, emit) async {
  final f = await ref.watch(fahrenheitCreator);
  final unit = ref.watch(unitCreator);
  emit(unit == 'Fahrenheit' ? '$f F' : '${f2c(f)} C');
});

// Fahrenheit to celsius converter.
int f2c(int f) => ((f - 32) * 5 / 9).round();

// main.dart

Widget build(BuildContext context) {
  return Watcher((context, ref, _) => 
      Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading'));
}
... context.ref.set(cityCreator, 'Pairs');  // triggers backend call
... context.ref.set(unitCreator, 'Celsius');  // doesn't trigger backend call

入门

dart pub add creator

目录

为什么选择 Creator?

当我们构建 Flutter 应用时,我们最初使用了 flutter_bloc。后来我们切换到了 riverpod。然而,我们在其异步 provider 中遇到了一些问题,并意识到我们需要一种不同的机制。

于是我们构建了 Creator。它在很大程度上受到 riverpod 的启发,但拥有更简单的数据模型、更好的异步支持以及更简化的实现。

使用 Creator 的好处

  • 使代码简洁、流畅、可读且可测试。
  • 对同步和异步行为都富有表现力。
  • 概念简单。实现简单。轻量级。

概念

Creator 的概念极其简单。只有两种类型的 creator

  • Creator,它创建一个 T 的流。
  • Emitter,它创建一个 Future<T> 的流。

这里的流是指逻辑意义上的流,而不是 Stream 类。

CreatorEmitter

  • 可以依赖其他 creator,并在其他 creator 的状态改变时更新自己的状态。
  • 是惰性加载,并自动处置。

依赖关系形成一个图,并由框架内部管理。下面是上面天气示例的图

weather

用法

Creator

Creator 接受一个您编写的用于创建状态的函数。该函数接受一个 Ref,它提供了与内部图交互的 API。

final number = Creator.value(42);  // Same as Creator((ref) => 42)
final double = Creator((ref) => ref.watch(number) * 2);

调用 watch 会在图中添加一条边 number -> double,因此当 number 的状态改变时,double 的创建函数会重新运行。

有趣的是,creator 提供了 mapwhere 等方法。它们与 Iterable 或 Stream 中的方法类似。所以 double 可以简单地这样写:

final double = number.map((n) => n * 2);

要更新 creator 的状态,请使用 setupdate

... ref.set(number, 42);  // No-op if value is the same
... ref.update(number, (n) => n + 10);

您也可以在 watch 不合适时 read 一个 creator,例如在触摸事件处理程序中。

TextButton(
  onPressed: () => print(context.ref.read(number)),
  child: const Text('Print'));

Creator 的依赖可以是动态的

final C = Creator((ref) {
  final value = ref.watch(A);
  return value >= 0 ? value : ref.watch(B);
});

在这个例子中,A -> C 总是存在的,B -> C 可能存在也可能不存在。框架会随着依赖的变化正确更新图。

Emitter

Emitter 的工作方式与 Creator 非常相似,但它创建的是 Future<T> 而不是 <T>。主要区别在于 Creator 最初就有有效数据,而 Emitter 可能需要等待一些异步工作才能产生第一个数据。

在实践中,Emitter 对于处理来自后端服务的数据非常有用,因为后端服务本质上是异步的。

final stockCreator = Creator.value('TSLA');
final priceCreator = Emitter((ref, emit) async {
  final stock = ref.watch(stockCreator);
  final price = await fetchStockPrice(stock);
  emit(price);
});

Emitter 接受一个 FutureOr<void> Function(Ref ref, void Function(T) emit),其中 ref 允许从图中获取数据,而 emit 允许将数据推回图中。您可以多次 emit

现有的 Stream 可以通过 Emitter.stream 轻松转换为 Emitter。它可以同步或异步工作

final authCreator = Emitter.stream(
    (ref) => FirebaseAuth.instance.authStateChanges());

final userCreator = Emitter.stream((ref) async {
  final authId = await ref.watch(
      authCreator.where((auth) => auth != null).map((auth) => auth!.uid));
  return FirebaseFirestore.instance.collection('users').doc(authId).snapshots();
});

这个例子还展示了扩展方法 wheremap。通过它们,userCreator 只会在 auth id 改变时重新创建,并忽略其他 auth 属性的变化。

在某种意义上,您可以将 Emitter 视为 Stream 的一个变体,这使得组合流变得非常容易。

Emitter 生成 Future<T>,因此可以将其连接到 Flutter 的 FutureBuilder 来构建 UI。或者您可以使用 Emitter.asyncData,它是一个 AsyncData<T> 的 creator。AsyncData 类似于 Future/Stream 的 AsyncSnapshot。

enum AsyncDataStatus { waiting, active }

class AsyncData<T> {
  const AsyncData._(this.status, this.data);
  const AsyncData.waiting() : this._(AsyncDataStatus.waiting, null);
  const AsyncData.withData(T data) : this._(AsyncDataStatus.active, data);

  final AsyncDataStatus status;
  final T? data;
}

这样使用 Emitter 构建小部件就很简单了

Watcher((context, ref, _) {
  final user = ref.watch(userCreator.asyncData).data;
  return user != null ? Text(user!.name) : const CircularProgressIndicator();
});

CreatorGraph

为了使 creators 生效,请将您的应用包装在 CreatorGraph

void main() {
  runApp(CreatorGraph(child: const MyApp()));
}

CreatorGraph 是一个 InheritedWidget。它持有一个 Ref 对象(该对象持有图),并通过 context.ref 将其暴露出来。

CreatorGraph 默认使用 DefaultCreatorObserver,它会在 creator 状态改变时打印日志。它可以被替换为您自己的日志收集观察者。

Watcher

Watcher 是一个 StatefulWidget。它接受一个构建器函数 Widget Function(BuildContext context, Ref ref, Widget child)。您可以使用 ref 来监视 creators 以填充小部件。child 可选,如果子树不应该在依赖项更改时重建。

Watcher((context, ref, child) {
  final color = ref.watch(userFavoriteColor);
  return Container(color: color, child: child);
}, child: ExpensiveAnimation());  // this child is passed into the builder above

监听变化

监视一个 creator 会获取其最新状态。如果您还想要前一个状态怎么办?只需调用 watch(someCreator.change) 来获取一个 Change<T>,它是一个具有 T? beforeT after 两个属性的对象。

为了方便起见,Watcher 也可以接受一个监听器

// If builder is null, child widget is directly returned. You can set both
// builder and listener. They are independent of each other.
Watcher(null, listener: (ref) {
  final change = ref.watch(number.change);
  print('Number changed from ${change.before} to ${change.after}');
}, child: SomeChildWidget());

名称

为了方便日志记录,Creators 可以有名称。对于任何严肃的应用,都建议设置名称。

final numberCreator = Creator.value(0, name: 'number');
final doubleCreator = numberCreator.map((n) => n * 2, name: 'double');

保持存活

默认情况下,当 creator 失去所有监视器时,它们会被处置。这可以通过 keepAlive 参数来覆盖。如果 creator 维护与后端的连接(例如监听 firestore 实时更新),这会很有用。

final userCreator = Emitter.stream((ref) {
  return FirebaseFirestore.instance.collection('users').doc('123').snapshots();
}, keepAlive: true);

Creator 分组

Creator 分组可以生成带有外部参数的 creators。例如,在 Instagram 应用中,导航堆栈上可能有多个个人资料页面,因此我们需要多个 profileCreator 实例。

// Instagram has four tabs: instagram, reels, video, tagged
final tabCreator = Creator.arg1<Tab, String>((ref, userId) => 'instagram');
final profileCreator = Emitter.arg1<Profile, String>((ref, emit, userId) async {
  final tab = ref.watch(tabCreator(userId));
  emit(await fetchProfileData(userId, tab));  // Call backend
});

// Now switching tab in user A's profile page will not affect user B.
... ref.watch(profileCreator('userA'));
... ref.set(tabCreator('userA'), 'reels');

Creators 带有工厂方法 arg1 arg2 arg3,它们接受 1-3 个参数。

扩展方法

我们最喜欢这个框架的部分是您可以使用 mapwhere 等方法处理 creators(完整列表请参见 此处)。它们与 Iterable 或 Stream 中的方法类似。

final numberCreator = Creator.value(0);
final oddCreator = numberCreator.where((n) => n.isOdd);

请注意,Creator 需要在开始时具有有效状态,而 where((n) => n.isOdd) 无法保证这一点。这就是为什么 where 返回一个 Emitter 而不是 Creator。这是 where 方法的实现。它非常简单,如果您愿意,也可以编写类似的扩展。

extension CreatorExtension<T> on Creator<T> {
  Emitter<T> where(bool Function(T) test) {
    return Emitter((ref, emit) {
      final value = ref.watch(this);
      if (test(value)) {
        emit(value);
      }
    });
  }
}

您可以使用两种方式的扩展方法

// Define oddCreator explicitly as a stable variable.
final oddCreator = numberCreator.where((n) => n.isOdd);
final someCreator = Creator((ref) {
  return 'this is odd: ${ref.watch(oddCreator)}');
})

// Create "oddCreator" anonymously on the fly.
final someCreator = Creator((ref) {
  return 'this is odd: ${ref.watch(numberCreator.where((n) => n.isOdd))}');
})

如果您使用“即时”方法,请阅读下一节关于 creator 相等性的内容。

Creator 相等性

图通过 == 检查两个 creators 是否相等。这意味着 creator 应该定义在全局变量、静态变量或其他可以保持变量在其生命周期内稳定的方式中。

如果在局部变量中即时定义 creators 会怎样?

final text = Creator((ref) {
  final double = Creator((ref) => ref.watch(number) * 2);
  return 'double: ${ref.watch(double)}';
})

这里 double 是一个局部变量,每当 text 被重新创建时,它都有不同的实例。内部图可能从 number -> double_A -> text 变为 number -> double_B -> text,因为数字在变化。text 仍然生成正确的数据,但图中有一个额外的成本来交换节点。由于变化仅限于一个节点,只要创建函数简单,就可以忽略该成本。

如果需要,可以设置一个可选的 List<Object?> args 来要求框架在图中找到一个具有相同 args 的现有 creator。现在当数字改变时,图不会改变。

final text = Creator((ref) {
  // args need to be globally unique. ['text', 'double'] is likely unique.
  final double = Creator((ref) => ref.watch(number) * 2, args: ['text', 'double']);
  return 'double: ${ref.watch(double)}';
})

在即时使用扩展方法也是如此。

final text = Creator((ref) {
  return 'double: ${ref.watch(number.map((n) => n * 2, args: ['text', 'double']))}';
})

内部而言,args 支持这些功能

  • Creator 分组。profileCreator('userA') 是一个带有 args [profileCreator, 'userA'] 的 creator。
  • 异步数据。userCreator.asyncData 是一个带有 args [userCreator, 'asyncData'] 的 creator。
  • 更改。number.change 是一个带有 args [number, 'change'] 的 creator。

服务定位器

状态管理库通常用作服务定位器

class UserRepo {
  void changeName(User user, String name) {...}
}
final userRepo = Creator.value(UserRepo());

... context.ref.read(userRepo).changeName(user, name);

如果需要,可以将 ref 传递给 UserRepo Creator((ref) => UserRepo(ref))。这允许 UserRepo readset 其他 creators。但不要 watch,因为它会导致 UserRepo 重建。

错误处理

框架将

  • 对于 Creator,存储创建过程中发生的异常,并在 watch 时抛出。
  • 对于 Emitter,自然地使用 Future.error,因此在 watch 时返回错误。
  • 在这两种情况下,错误都被视为状态更改。

这意味着错误可以以最自然的方式处理,在最合适的地方处理。以上面的天气应用为例

// Here we don't handle error, meaning it returns Future.error if network error
// occurs. Alternately we can catch network error and return some default value,
// add retry logic, convert network error to our own error class, etc.
final fahrenheitCreator = cityCreator.mapAsync(getFahrenheit);

// Here we choose to handle the error in widget.
Widget build(BuildContext context) {
  return Watcher((context, ref, _) {
    try {
      return Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading');
    } catch (error) {
      return TextButton('Something went wrong, click to retry', 
          onPressed: () => ref.recreate(fahrenheitCreator));
    }
  };
}

测试

通过结合 watchreadset 来测试 creator 非常简单。以上面的天气应用为例

test('temperature creator change unit', () async {
  final ref = Ref();
  expect(await ref.watch(temperatureCreator), "60 F");
  ref.set(unitCreator, 'Celsius');
  await Future.delayed(const Duration()); // allow emitter to propagate
  expect(await ref.watch(temperatureCreator), "16 C");
});

test('temperature creator change fahrenheit value', () async {
  final ref = Ref();
  expect(await ref.watch(temperatureCreator), "60 F");
  ref.set(fahrenheitCreator, Future.value(90));
  await Future.delayed(const Duration()); // allow emitter to propagate
  expect(await ref.watch(temperatureCreator), "90 F");
});

示例

计数器

DartPad

一个计数器应用展示了基本的 Creator/Watcher 用法。

Weather

DartPad

一个简单的天气应用展示了如何分割后端/逻辑/UI 代码,以及如何使用 Creator 和 Emitter 编写逻辑。

Graph

DartPad

一个简单的应用展示了 creator 框架如何动态构建内部图。

最佳实践

Creator 非常灵活,并且不强制特定的风格。最佳实践也取决于项目和个人偏好。这里我们只列出一些我们遵循的内容

  • 将代码分成 repo 文件(后端服务调用)、逻辑文件(creator)和 UI 文件(widget)。
  • 在全局变量中定义 creator。
  • 保持 creator 小巧以便于测试。将派生状态放在派生 creators 中(使用 mapwhere 等)。

阅读源代码

Creator 的实现非常简单。事实上,核心逻辑的代码量不到 500 行。

请按此顺序阅读 creator_core 库

  • graph.dart:一个简单的使用邻接表实现的双向图。它可以自动删除出度为零的节点。
  • creator.dart:CreatorBase 类及其两个子类 Creator 和 Emitter。它们的主要工作是在被要求时重新创建状态。
  • ref.dart:管理图并向用户提供 watchreadset 方法。
  • extension.dart:实现扩展方法 mapwhere 等。

请按此顺序阅读 creator 库

  • creator_graph.dart:一个简单的 InheritedWidget,通过 context 公开 Ref
  • watcher.dart:一个内部包含 Creator<Widget> 的有状态小部件。

常见问题

它是否已准备好投入生产?

嗯,我们已经在自己的应用(Chooly)中将其用于生产环境。但是,由于它对社区来说是新的,API 可能会随着我们收集反馈而改变。所以目前的建议是:阅读源代码并自行判断。

将 creator 定义为全局变量是否不好?

不是。Creator 本身不持有状态。状态由 Ref(在 CreatorGraph 中)持有。定义一个 creator 更像是定义一个函数或一个类。

如何在监视一个属性的同时仍然访问整个对象?

final someCreator = Creator((ref) {
  ref.watch(userCreator.map((user) => user.email));
  final user = ref.read(userCreator);
  return '${user.name}\'s email is changed';
});

Creator 的生命周期是如何工作的?

  • 它在第一次被监视时被添加到图中。
  • 它可以由 Ref.dispose 手动从图中删除。
  • 如果它有监视器,除非设置了 keepAlive 属性,否则当它失去所有监视器时会自动从图中删除。

context.refCreator((ref) => ...) 中的 ref 有什么区别?

它们都指向同一个内部图,唯一的区别是第一个 ref 的 _owner 字段为 null,而第二个 ref 的 _owner 字段是 creator 本身。这意味着

  • 使用任一 ref 来 readsetupdate 任何 creators 都是相同的。操作会传递给内部图。
  • 如果 ref._owner 为 null,ref.watch(foo) 将简单地将 foo 添加到图中。
  • 如果 ref._owner 不为 null,ref.watch(foo) 还会将一条边 foo -> ref._owner 添加到图中。

Creator<Future<T>>Emitter<T> 有什么区别?

它们都继承自 CreatorBase<Future<T>>,其状态是 Future<T>。但是,有两个重要区别,这使得 Emitter<T> 更适合异步任务

  • Emitter<T> 除了 Future<T> 之外还存储 T,以便我们可以记录 T 的变化或正确填充 AsyncData<T>
  • Emitter<T> 在发出 T 时通知其监视器,因此其监视器可以立即开始工作。Creator<Future<T>> 在 Future 启动时通知其监视器,因此其监视器在 Future 完成之前仍然被阻塞。

就是这样

希望您喜欢阅读这篇文档,并会喜欢使用 Creator。欢迎提供反馈和贡献!

GitHub

查看 Github