与使用内置的FutureBuilder 和StreamBuilder Flutter小部件相比,使用Riverpod包处理异步数据是一件很容易的事情。
// A widget that shows product data for a given product ID
class ProductScreen extends ConsumerWidget {
const ProductScreen({Key? key, required this.productId}) : super(key: key);
final String productId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// get an async value from a StreamProvider
final productAsyncValue = ref.watch(productProvider(productId));
// return different "data", "loading", "error" widgets depending on the value
return productAsyncValue.when(
data: (product) => ProductScreenContents(product: product),
loading: (_) => const Center(child: CircularProgressIndicator()),
error: (e, _, __) => Center(
child: Text(
e.toString(),
style: Theme.of(context)
.textTheme
.headline6!
.copyWith(color: Colors.red),
),
),
);
}
}
// NOTE: using StreamProvider.family so we can pass a custom "id" at runtime
final productProvider =
StreamProvider.autoDispose.family<Product, String>((ref, id) {
// dataStore is an API we can use to access our DB
final dataStore = ref.watch(dataStoreProvider);
// return a stream with the given product ID
return dataStore.product(id);
});
所有这些魔法都是可能的,因为when 方法给了我们一个方便的模式匹配API,我们可以用它来把我们的数据映射到UI上。
注意上面的代码在运行时使用一个所谓的家族来传递
productId。关于这一点的更多信息,请阅读我的Flutter Riverpod基本指南中的family修改器。
不要重复自己的工作
当你构建你的应用程序时,你很可能为不同的异步API设置不同的 "数据 "小部件。
productAsyncValue.when(
// this changes depending on which API we're using
data: (product) => ProductScreenContents(product: product),
// this is always the same
loading: (_) => const Center(child: CircularProgressIndicator()),
// this is always the same
error: (e, _, __) => Center(
child: Text(
e.toString(),
style: Theme.of(context)
.textTheme
.headline6!
.copyWith(color: Colors.red),
),
),
)
但是,加载和错误的UI往往是相同的,如果每次我们需要一个新的 "异步 "小部件时都复制粘贴它们,那就相当重复了。
DRY的解决方案。异步价值部件(AsyncValueWidget
一个更好的选择是定义一个AsyncValueWidget ,来处理加载和错误状态,并让我们为数据状态定制UI。
这很容易实现。
// Generic AsyncValueWidget to work with values of type T
class AsyncValueWidget<T> extends StatelessWidget {
const AsyncValueWidget({Key? key, required this.value, required this.data})
: super(key: key);
// input async value
final AsyncValue<T> value;
// output builder function
final Widget Function(T) data;
@override
Widget build(BuildContext context) {
return value.when(
data: data,
loading: (_) => const Center(child: CircularProgressIndicator()),
error: (e, _, __) => Center(
child: Text(
e.toString(),
style: Theme.of(context)
.textTheme
.headline6!
.copyWith(color: Colors.red),
),
),
);
}
}
有了这个,我们可以像这样重写我们的ProductScreen 。
class ProductScreen extends ConsumerWidget {
const ProductScreen({Key? key, required this.productId}) : super(key: key);
final String productId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final productAsyncValue = ref.watch(productProvider(productId));
return AsyncValueWidget<Product>(
value: productAsyncValue,
data: (product) => ProductScreenContents(product: product),
);
}
}
这样就干净多了。
分割器怎么办?
AsyncValueWidget 类对于普通的widget来说效果不错。但有时你有复杂的视图层次,需要使用分流器。
例如,这里有一个ProductsList widget,它是作为CustomScrollView 中的一个分流器使用的。
class ProductsList extends ConsumerWidget {
const ProductsList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsValue = ref.watch(productsProvider);
return productsValue.when(
// data
data: (products) => SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final product = products[index];
return ProductCard(product: product);
},
childCount: products.length,
),
),
// loading UI with SliverToBoxAdapter
loading: (_) => const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator())),
// error UI with SliverToBoxAdapter
error: (e, _, __) => SliverToBoxAdapter(
child: Center(
child: Text(
e.toString(),
style: Theme.of(context)
.textTheme
.headline6!
.copyWith(color: Colors.red),
),
),
),
);
}
}
// use like this:
CustomScrollView(
slivers: [
SliverToBoxAdapter(
// title
child: Text('Products List',
style: Theme.of(context).textTheme.headline4),
)
// contents
const ProductsList(),
// optionally, add some other slivers
]
)
在这种情况下,加载和错误部件都需要被包裹在一个SliverToBoxAdapter 。
但是我们的AsyncValueWidget 并没有这样做,如果我们试图在我们的ProductsList 内使用它,我们会得到这个错误。
A RenderViewport expected a child of type RenderSliver but received a child of type RenderPositionedBox.
解决这个问题的方法是创建一个能做正确事情的AsyncValueSliverWidget 。
class AsyncValueSliverWidget<T> extends StatelessWidget {
const AsyncValueSliverWidget(
{Key? key, required this.value, required this.data})
: super(key: key);
// input async value
final AsyncValue<T> value;
// output builder function
final Widget Function(T) data;
@override
Widget build(BuildContext context) {
return value.when(
data: data,
loading: (_) => const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator())
),
error: (e, _, __) => SliverToBoxAdapter(
child: Center(
child: Text(
e.toString(),
style: Theme.of(context)
.textTheme
.headline6!
.copyWith(color: Colors.red),
),
),
),
);
}
}
然后,我们可以像这样使用它。
class ProductsList extends ConsumerWidget {
const ProductsList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsValue = ref.watch(productsProvider);
return AsyncValueSliverWidget<List<Product>>(
value: productsValue,
data: (products) => SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final product = products[index];
return ProductCard(product: product);
},
childCount: products.length,
),
),
);
}
}
好多了。
结论
上面定义的两个AsyncValueWidget 和AsyncValueSliverWidget 实用类帮助我们使我们的代码更加干燥。
这为我们节省了几行代码,如果我们决定改变我们的加载和错误用户界面的风格,我们只需要在一个地方做。
这似乎是一个小的胜利,但这一切都增加了。
编码愉快!