[译][官方文档] Flutter/Dart 状态管理库 Riverpod - 场景学习 - 下拉刷新

359 阅读4分钟

原文链接:Pull to refresh | Riverpod

pub:riverpod | Dart Package (flutter-io.cn)

译时版本: 2.4.9


之前翻译过 Riverpod 的官方文档,现在随着版本更新,官方文档又多了很多新内容,所以再补充翻译一下。
之前翻译过的内容,现在官方文档有中文了。
Flutter状态管理库Riverpod官方文档翻译汇总 - 掘金 (juejin.cn)


下拉刷新

Riverpod 得益于其声明式的属性天生支持下拉刷新。

通常,下拉刷新都很复杂,因其需要解决多个问题:

  • 第一次进入某个画面时,想要表示一个加载中的指示器。但是在刷新时,是想要表示刷新指示器。不应该同时表示刷新指示器加载中的指示器。
  • 还没开始刷新时,需要表示之前的数据/错误。
  • 需要在刷新过程中一直表示刷新指示器。

看一下如何用 Riverpod解决这个问题。
对于该问题,会创建一个简单示例,该示例会向用户推荐一个随机 activity 。
下拉刷新会触发一个新的建议:

A gif of the previously described application working

创建一个应用的基本框架

实现下拉刷新之前,首先还需要一些准备用于刷新的内容。
可以创建一个简单的应用,该应用使用 Bored API 向用户推荐一个随机 activity 。

首先,定义 Activity 类:

@freezed
class Activity with _$Activity {
  factory Activity({
    required String activity,
    required String type,
    required int participants,
    required double price,
  }) = _Activity;

  factory Activity.fromJson(Map<String, dynamic> json) =>
      _$ActivityFromJson(json);
}

该类用于表现建议的activity,它是以类型安全的方式,并处理 JSON 编解码。
使用 Freezed/json_serializable 不是必须的,但是推荐这么做。

现在,是要定义一个 provider ,以创建 HTTP GET 请求获取单个 activity:

@riverpod
Future<Activity> activity(ActivityRef ref) async {
  final response = await http.get(
    Uri.https('www.boredapi.com', '/api/activity'),
  );

  final json = jsonDecode(response.body) as Map;
  return Activity.fromJson(Map.from(json));
}

现在要用这个 provider 来显示一个随机 activity 。
现在还不处理加载中/错误状态,只是简单地在 activity 可用时将其显示:

class ActivityView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activity = ref.watch(activityProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Pull to refresh')),
      body: Center(
        // 如果有 activity 就显示,否则继续等待。
        child: Text(activity.valueOrNull?.activity ?? ''),
      ),
    );
  }
}

添加 RefreshIndicator (刷新指示器)

现在有了一个简单的应用,可以向其添加 RefreshIndicator (刷新指示器)。
该组件是官方的 Material 组件,用于在用户下拉屏幕时显示一个刷新的指示器。

使用 RefreshIndicator 需要一个可滚动界面。但是至今还没有。可以使用 ListView/GridView/SingleChildScrollView 等来修改:

class ActivityView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activity = ref.watch(activityProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Pull to refresh')),
      body: RefreshIndicator(
        onRefresh: () async => print('refresh'),
        child: ListView(
          children: [
            Text(activity.valueOrNull?.activity ?? ''),
          ],
        ),
      ),
    );
  }
}

现在用户可以下拉屏幕了。但是数据还不会刷新。

添加刷新逻辑

当用户下拉屏幕时,RefreshIndicator 会调用 onRefresh 回调。可以利用该回调刷新数据。在回调中,使用 ref.refresh 来刷新所选择的 provider 。

注意onRefresh 的期待值是返回一个 Future 。所以 future 在刷新结束时完成是很重要的.

要获得这样的特性,可以读取 provider 的 .future 属性。
这会返回一个 future ,在 provider 解决时,该 future 将会完成。

因此就能如下更新 RefreshIndicator 了:

class ActivityView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activity = ref.watch(activityProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Pull to refresh')),
      body: RefreshIndicator(
        // 刷新 "activityProvider.future" ,返回其结果,
        // 刷新指示器会一直表示直到获取到新的 activity 。
        onRefresh: () => ref.refresh(activityProvider.future),
        child: ListView(
          children: [
            Text(activity.valueOrNull?.activity ?? ''),
          ],
        ),
      ),
    );
  }
}

只在初始加载和处理错误时表示加载指示器

此时,UI不会处理错误/加载中状态。
加载中/刷新完成时数据会魔法般出现。

修改为优雅地处理这些状态。有两种情况:

  • 在初始化加载时,想表示一个全屏的指示器。
  • 刷新时,想表示刷新指示器和之前的数据/错误。

幸运的是,在 Riverpod 中监听一个异步的 provider 时,Riverpod 会给予一个 AsyncValue ,它提供了所需的所有内容。

AsyncValue 之后可如下用 Dart 3.0 的模式匹配进行绑定:

class ActivityView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activity = ref.watch(activityProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Pull to refresh')),
      body: RefreshIndicator(
        onRefresh: () => ref.refresh(activityProvider.future),
        child: ListView(
          children: [
            switch (activity) {
              // 如果有数据可用,就显示它。
              // 注意数据在刷新期间会一直可用。
              AsyncValue<Activity>(:final valueOrNull?) =>
                Text(valueOrNull.activity),
              // 发生错误,就渲染它。
              AsyncValue(:final error?) => Text('Error: $error'),
              // 没有数据/错误,就是在加载中的状态。
              _ => const CircularProgressIndicator(),
            },
          ],
        ),
      ),
    );
  }
}

警告

这里使用 valueOrNull ,现在使用 value 如果是错误/加载中状态,会抛出异常。

Riverpod 3.0 会修改为 value ,其行为像 valueOrNull 。 但是现在,先继续使用 valueOrNull

提示

注意模式匹配中 :final valueOrNull? 语法的用法。该语法仅在 activityProvider 返回一个不为空的 Activity 时使用。

如果数据可能为 null ,需要使用 AsyncValue(hasData: true, :final valueOrNull) 代替。这会正确地处理数据为 null 时的情况,代价是一些多余字符的处理成本。

封装:完整应用

下面是覆盖上面所有内容对应的代码:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'codegen.g.dart';
part 'codegen.freezed.dart';

void main() => runApp(ProviderScope(child: MyApp()));

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: ActivityView());
  }
}

class ActivityView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activity = ref.watch(activityProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Pull to refresh')),
      body: RefreshIndicator(
        onRefresh: () => ref.refresh(activityProvider.future),
        child: ListView(
          children: [
            switch (activity) {
              AsyncValue<Activity>(:final valueOrNull?) =>
                Text(valueOrNull.activity),
              AsyncValue(:final error?) => Text('Error: $error'),
              _ => const CircularProgressIndicator(),
            },
          ],
        ),
      ),
    );
  }
}

@riverpod
Future<Activity> activity(ActivityRef ref) async {
  final response = await http.get(
    Uri.https('www.boredapi.com', '/api/activity'),
  );

  final json = jsonDecode(response.body) as Map;
  return Activity.fromJson(Map.from(json));
}

@freezed
class Activity with _$Activity {
  factory Activity({
    required String activity,
    required String type,
    required int participants,
    required double price,
  }) = _Activity;

  factory Activity.fromJson(Map<String, dynamic> json) =>
      _$ActivityFromJson(json);
}