Flutter Riverpod提示|使用AsyncValue而不是FutureBuilder或StreamBuilder

1,075 阅读6分钟

如果你已经使用Flutter一段时间了,你会遇到FutureBuilderStreamBuilder --两个方便的小工具,用于处理你UI中的异步数据。

这两个小工具都迫使你使用 AsyncSnapshot,一个Flutter文档中定义的类。

最近一次与异步计算的交互的不可改变的表示。

呃,那是什么?

让我用我自己的话再试一次。

AsyncSnapshot 表示一个异步值,可以包含一些数据加载状态错误状态

虽然这更有意义,但与AsyncSnapshot ,在实践中是一个有点痛苦的工作。

所以在这篇文章中,我们将了解到 AsyncValue,这是对AsyncSnapshot 的一个更好的替代。

AsyncValue 在Flutter中,Riverpod是一个广泛的状态管理库。你可以在官方网站上了解更多信息,或者阅读我的Riverpod基本指南

FutureBuilder和StreamBuilder有什么问题?

FutureBuilderStreamBuilder小工具有一个非常相似的API,并且接受两个参数。

  • 一个输入值(可以是一个Future ,也可以是一个Stream
  • 一个构建器函数,我们可以用它来 "映射 "一个异步快照到一个小组件上
FutureBuilder(
  future: someFuture,
  builder: (context, snapshot) {
    // check snapshot for loading, data, and errors
    // and return a widget
  },
);

StreamBuilder(
  stream: someStream,
  builder: (context, snapshot) {
    // check snapshot for loading, data, and errors
    // and return a widget
  },
);

然而,一旦你开始实施构建器代码,你很快就会发现,与那个讨厌的快照打交道并不怎么有趣。😭

一个 "简单 "的StreamBuilder例子

假设你有一个简单的Item 模型类,同时还有一个返回Stream<Item> 的函数。

class Item {
  const Item({required this.title, required this.description});
  final String title;
  final String description;
}

// this could represent data that comes from a realtime database
Stream<Item> getItem() { ... }

如果你想使用StreamBuilder ,每当有新的流值发出来时,就重建你的用户界面,这就是你必须写的代码。

StreamBuilder<Item>(
  stream: getItem(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      // handle loading
      return const Center(child: CircularProgressIndicator());
    } else if (snapshot.hasData) {
      // handle data
      final item = snapshot.data!;
      return ListTile(
        title: Text(item.title),
        subtitle: Text(item.description),
      );
    } else if (snapshot.hasError) {
      // handle error (note: snapshot.error has type [Object?])
      final error = snapshot.error!;
      return Text(error.toString());
    } else {
      // uh, oh, what goes here?
      return Text('Some error occurred - welp!');
    }
  },
);

这不是很好,因为你需要检查至少三个不同的属性(connectionState,data, 和error )来处理所有的情况,但如果你忘记这样做,编译器不会警告你。

在实践中,加载错误数据状态应该是相互排斥的,但AsyncSnapshot API没有办法表达这一点。

事实上,如果你偷看一下Flutter SDK中它的定义,你会发现它有这些属性。

class AsyncSnapshot<T> {
  final ConnectionState connectionState;
  final T? data;
  final Object? error;
  final StackTrace? stackTrace;
  bool get hasData => data != null;
  bool get hasError => error != null;
}

这里的主要问题是,connectionState,data,error,stackTrace 等变量都是相互独立的。但是对于应该是相互排斥的状态来说,这并不是一个好的表示。

这就是导致我们的构建器中所有的if-else语句的原因。

真正的问题:Dart没有密封的联合体

我们要做的事情并不容易,因为Dart语言还没有对密封的联合体提供适当的支持(目前)。

但我们可以做得比这更好,即使在目前的语言限制下。而来自Riverpod包的AsyncValue 类向我们展示了它是如何做到的。👇

AsyncValue作为AsyncSnapshot的替代品

AsyncValue 类使用工厂构造函数来定义三个互斥的状态:数据加载错误

abstract class AsyncValue<T> {
  const factory AsyncValue.data(T value) = AsyncData<T>;
  const factory AsyncValue.loading() = AsyncLoading<T>;
  const factory AsyncValue.error(Object error, {StackTrace? stackTrace}) =
      AsyncError<T>;
}

而且它给我们一个 AsyncValueX 扩展和各种模式匹配方法,我们可以用这些方法将这些状态映射到用户界面上。

这意味着我们可以这样做。

import 'package:flutter_riverpod/flutter_riverpod.dart';

final itemStreamProvider = StreamProvider<Item>((ref) {
  return getItem(); // returns a Stream<Item>
});

class ItemWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // get the AsyncValue<Item> by watching the StreamProvider
    final itemValue = ref.watch(itemStreamProvider);
    // map all its states to widgets and return the result
    return itemValue.when(
      data: (item) => ListTile(
        title: Text(item.title),
        subtitle: Text(item.description),
      ),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, st) => Center(child: Text(e.toString())),
    );
  }
);

有一件事有时会让人困惑,那就是AsyncValue (很像AsyncSnapshot只是一个值,而不是一个可听对象。而正是对ref.watch(provider) 的调用导致了小部件的重建

这就更简洁了,而且对自动完成的工作也很好。🚀

如果我们将行数与StreamBuilder 的例子进行比较,其差异是相当明显的。

在StreamBuilder和AsyncValue之间进行逐行比较


AsyncValue 可能是自切片面包以来最好的发明。🚀

而如果你想使用它,你需要遵循一些设置步骤。

让我们来看看。👇

1.Riverpod的安装和设置

当然,只有在你的Flutter应用中使用Riverpod,你才能利用AsyncValue

所以一定要把它添加到你的项目中。

flutter pub add flutter_riverpod

这将在你的pubspec.yaml 文件中把它作为一个依赖项。

dependencies:
  flutter_riverpod: ^1.0.3

而且你还必须记得用ProviderScope 来包装你的整个应用程序。

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

2.根据需要创建一个StreamProvider或FutureProvider

由于内置的FutureBuilderStreamBuilder 小工具直接将FutureStream 作为参数,使用它们不需要额外的步骤(只要你能访问FutureStream 本身)。

但是在Riverpod中,你想在用户界面中使用的每个FutureStream 都应该有自己的提供者

final itemStreamProvider = StreamProvider<Item>((ref) {
  // return a Stream<Item>
});

final itemFutureProvider = FutureProvider<Item>((ref) {
  // return a Future<Item>
});

3.创建一个ConsumerWidget并观察你的提供者

这也意味着你必须将你的widget类转换为ConsumerWidget ,并在build() 方法中获得一个额外的WidgetRef 参数。

// base class is now [ConsumerWidget]
class SomeWidget extends ConsumerWidget {
  // build method now gets a ref argument
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final streamAsyncValue = ref.watch(itemStreamProvider);
    // final futureAsyncValue = ref.watch(itemFutureProvider);
    return streamAsyncValue.when(...);
  }
}

这样,你就可以调用ref.watch(provider) ,每当有新的AsyncValuewidget就会重新构建

Riverpod是一个广泛的状态管理库,有很多功能。如需完整的概述,请参阅我的Riverpod基本指南

结论

正如我们所看到的,内置的 FutureBuilderStreamBuilder小工具给了我们一个AsyncSnapshot ,我们可以用它来处理用户界面中的异步数据。

但这在实践中是很难使用的,而 AsyncValue类提供了一个更符合人体工程学的API。

而如果你的应用中有大量的异步数据,你可以在AsyncValue 的基础上创建一些辅助类来进一步增加你的里程。

而这些文章涵盖了所有的细节。