一个可重复使用的用于处理AsyncValue(使用Riverpod)的Flutter部件

457 阅读3分钟

与使用内置的FutureBuilderStreamBuilder 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,
        ),
      ),
    );
  }
}

好多了。

结论

上面定义的两个AsyncValueWidgetAsyncValueSliverWidget 实用类帮助我们使我们的代码更加干燥。

这为我们节省了几行代码,如果我们决定改变我们的加载和错误用户界面的风格,我们只需要在一个地方做。

这似乎是一个小的胜利,但这一切都增加了。

编码愉快!