如果你已经使用Flutter一段时间了,你会遇到FutureBuilder 和StreamBuilder --两个方便的小工具,用于处理你UI中的异步数据。
这两个小工具都迫使你使用 AsyncSnapshot,一个Flutter文档中定义的类。
最近一次与异步计算的交互的不可改变的表示。
呃,那是什么?
让我用我自己的话再试一次。
AsyncSnapshot表示一个异步值,可以包含一些数据、加载状态或错误状态。
虽然这更有意义,但与AsyncSnapshot ,在实践中是一个有点痛苦的工作。
所以在这篇文章中,我们将了解到 AsyncValue,这是对AsyncSnapshot 的一个更好的替代。
AsyncValue在Flutter中,Riverpod是一个广泛的状态管理库。你可以在官方网站上了解更多信息,或者阅读我的Riverpod基本指南。
FutureBuilder和StreamBuilder有什么问题?
这 FutureBuilder和 StreamBuilder小工具有一个非常相似的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
由于内置的FutureBuilder 和StreamBuilder 小工具直接将Future 或Stream 作为参数,使用它们不需要额外的步骤(只要你能访问Future 或Stream 本身)。
但是在Riverpod中,你想在用户界面中使用的每个Future 或Stream 都应该有自己的提供者。
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) ,每当有新的AsyncValue ,widget就会重新构建。
Riverpod是一个广泛的状态管理库,有很多功能。如需完整的概述,请参阅我的Riverpod基本指南。
结论
正如我们所看到的,内置的 FutureBuilder和 StreamBuilder小工具给了我们一个AsyncSnapshot ,我们可以用它来处理用户界面中的异步数据。
但这在实践中是很难使用的,而 AsyncValue类提供了一个更符合人体工程学的API。
而如果你的应用中有大量的异步数据,你可以在AsyncValue 的基础上创建一些辅助类来进一步增加你的里程。
而这些文章涵盖了所有的细节。