Riverpod 3 :掌握异步任务处理与 AsyncNotifier

为什么我们需要更现代的异步处理方式?

在任何一个 Flutter 应用中,异步操作都无处不在:从网络请求、数据库读写到文件 IO,它们是构建动态用户体验的基石。然而,管理这些异步任务的状态------加载中、成功、失败------常常是开发中最核心也最容易出错的环节。

你是否也曾陷入这样的困境?在 Riverpod 的早期版本中,我们通常组合使用 FutureProvider 来执行一次性请求,再配合 StateNotifier 来处理后续的用户交互和状态变更。这种方式不仅需要编写大量模板代码(Boilerplate),而且随着业务逻辑的复杂化,状态管理会变得支离破碎,难以维护。

不过,之后推出的Riverpod (2.0+) 带来了革命性的 AsyncNotifier ,它与代码生成器强强联合,彻底改变了游戏规则。接下来我们将一起深入探索 AsyncNotifier ,学习如何利用它以及相关的 AsyncValue 模型,编写出更简洁、更健壮、更优雅的异步状态管理代码。

核心 API:AsyncNotifier 与 AsyncNotifierProvider

AsyncNotifier 是什么?

简单来说,AsyncNotifier 是一个内置了异步处理能力的 Notifier。它专门用于封装那些需要异步初始化的状态。其核心在于一个异步的 build 方法,你可以在其中执行任意异步任务(如 API 请求),并将最终结果作为 provider 的初始状态。它将过去分散的异步逻辑和状态变更操作统一到一个地方,让代码权责分明。

使用方式:拥抱 @riverpod 代码生成

代码生成是现代 Riverpod 的一大特色,它能极大简化 Provider 的声明。下面是一获取用户信息的典型例子:

dart 复制代码
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'packagea:flutter_riverpod/flutter_riverpod.dart';

part 'user_profile_provider.g.dart';

// 定义用户数据模型
class UserProfile {
  final String name;
  UserProfile({required this.name});
}

// 模拟一个网络请求
Future<UserProfile> fetchUserProfile() async {
  // 模拟 2 秒网络延迟
  await Future.delayed(const Duration(seconds: 2));
  // 模拟成功或失败
  if (DateTime.now().second % 2 == 0) {
    return UserProfile(name: 'TechFlow');
  } else {
    throw Exception('Failed to fetch user profile.');
  }
}

//使用 @riverpod 注解声明 Provider
@riverpod
class UserProfileController extends _$UserProfileController {

  @override
  Future<UserProfile> build() async {
    // 这里执行异步任务,返回值将成为初始的 state.data
    return fetchUserProfile();
  }

  //定义其他方法来修改状态,例如刷新
  Future<void> refresh() async {
    // 手动将状态设置为加载中,UI会立刻响应
    state = const AsyncLoading();
    // 使用 AsyncValue.guard 安全地执行异步操作并更新状态
    // 它会自动捕获异常并转换为 AsyncError
    state = await AsyncValue.guard(() => fetchUserProfile());
  }
}

代码解读:

  • @riverpod 注解:告诉代码生成器为 UserProfileController 创建一个名为 userProfileControllerProvider 的 Provider。
  • extends _$UserProfileController:继承自代码生成器创建的基类,这是固定写法。
  • Future build() :这是 AsyncNotifier 的核心。它的返回值 Future 会被 Riverpod 自动监听。在 Future 完成前,provider 的状态是 AsyncLoading ;完成后,如果成功,状态变为 AsyncData ,如果失败,则变为 AsyncError

在 UI 中消费 AsyncValue 状态

AsyncNotifier 的状态被封装在 AsyncValue 对象中。在 UI 层,我们可以使用 ref.watch 来监听这个状态,并利用其强大的 .when 方法,优雅地处理所有可能的情况。

less 复制代码
@override
Widget build(BuildContext context, WidgetRef ref) {
  // 监听 Provider 的状态
  final asyncUserProfile = ref.watch(userProfileControllerProvider);

  // 使用 .when 方法,根据状态构建不同的 UI
  return asyncUserProfile.when(
    // 状态为:加载中
    loading: () => const Center(child: CircularProgressIndicator()),
    
    // 状态为:发生错误
    error: (error, stackTrace) => Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('加载失败: $error'),
          const SizedBox(height: 16),
          ElevatedButton(
            // 调用 Notifier 中的 refresh 方法来重试
            onPressed: () {
              ref.read(userProfileControllerProvider.notifier).refresh();
            },
            child: const Text('点击重试'),
          ),
        ],
      ),
    ),

    // 状态为:数据加载成功
    data: (profile) => Center(
      child: Text(
        '你好, ${profile.name}!',
        style: Theme.of(context).textTheme.headlineMedium,
      ),
    ),
  );
}

这种写法逻辑清晰、代码健壮,覆盖了异步操作的所有场景。

AsyncValue 的状态范式

AsyncValue 是 Riverpod 为异步状态设计的完美抽象,它只有三种具体状态:

  • AsyncLoading :异步操作正在进行中。
  • AsyncData :异步操作成功完成,并持有数据 T
  • AsyncError :异步操作失败,并包含错误信息 error 和堆栈 stackTrace。

在 UI 中,强烈推荐使用 .when 方法来处理 AsyncValue,它能确保你不会遗漏任何一种状态。如果你只想处理其中一两种状态,可以使用 .maybeWhen。

高级特性:可靠性与资源管理

实现自定义重试逻辑

虽然 Riverpod 没有内置的"自动指数退避重试"功能,但我们可以在 build 方法中轻松实现它。当 build 方法失败后,provider 会进入 error 状态。如果此时有新的监听者,或者我们调用 ref.invalidate ,build 方法就会被重新执行,这天然地为我们提供了重试的基础。

下面是一个在 build 方法内实现带延迟的、有限次数重试的例子:

dart 复制代码
@riverpod
class WeatherProvider extends _$WeatherProvider {
  @override
  Future<Weather> build(String city) async {
    const maxRetries = 3; // 最多重试 3 次
    for (int i = 0; i < maxRetries; i++) {
      try {
        return await weatherApi.fetch(city); // 尝试请求
      } catch (e) {
        // 如果是最后一次尝试,则直接抛出异常
        if (i == maxRetries - 1) {
          rethrow;
        }
        // 等待一段时间再重试 (例如 2 秒)
        await Future.delayed(const Duration(seconds: 2));
      }
    }
    // 理论上不会执行到这里,加一个保障
    throw Exception('Failed to fetch weather after $maxRetries retries.');
  }
}

通过 .autoDispose 管理资源

现代 Riverpod 中,节约资源的核心机制是 .autoDispose 修饰符。默认情况下,使用 @riverpod 注解生成的 provider 都是 autoDispose 的。

工作原理 : 当一个 autoDispose provider 不再有任何监听者时(例如,依赖它的页面被销毁),Riverpod 会自动销毁该 provider 的状态,释放内存和资源。当它下次被需要时,会重新执行 build 方法进行初始化。这就像是房间"人走灯灭",非常智能,可以有效防止内存泄漏和不必要的后台活动。   如果我们希望 provider 的状态在任何时候都保持存活(类似单例),可以使用 @Riverpod(keepAlive: true)

统一的错误封装与稳定性

Riverpod 会将 provider 内部抛出的所有异常都捕获并封装成 ProviderException 。这样做的好处是,我们总能从这个异常对象中获取到原始的异常信息和完整的堆栈跟踪,极大地方便了调试。   在 UI 层,我们通常不需要手动 try-catch,因为 AsyncValue.whenerror 分支已经帮我们处理好了。但在某些逻辑代码中(例如在一个 Notifier 方法中读取另一个 provider 的值),你可能需要手动处理:

dart 复制代码
Future<void> someOtherAction() async {
  try {
    // .future 会返回一个 Future,如果 provider 处于 error 状态,它会 rethrow 异常
    final user = await ref.read(userProfileControllerProvider.future);
    // ... 使用 user 数据
  } on ProviderException catch (e) {
    // 这里可以获取到原始的异常
    if (e.exception is NetworkException) {
      // 进行针对性的处理
      print('网络错误: ${e.exception}');
    } else {
      print('未知错误: ${e.exception}');
    }
  }
}

实战案例:天气查询应用

让我们用一个简单的天气查询功能,将所有知识点串联起来。

定义 WeatherController (AsyncNotifier)

scala 复制代码
@riverpod
class WeatherController extends _$WeatherController {
  @override
  Future<Weather> build(String city) async {
    // build 方法接收一个参数,用于查询特定城市的天气
    return await WeatherApi.fetch(city);
  }

  // 我们不再需要手写 refresh 或 retry 方法,
  // 因为 'ref.invalidate' 可以更优雅地实现这个功能。
}

UI 逻辑

less 复制代码
//城市
@riverpod
class CityController extends _$CityController {
  @override
  String build() => '北京';

  void updateCity(String newCity) {
    state = newCity;
  }
}
//天气页面
class WeatherPage extends ConsumerWidget {
  const WeatherPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    String city = ref.watch(cityControllerProvider); // 示例城市
    final asyncWeather = ref.watch(weatherControllerProvider(city));

    return Scaffold(
      appBar: AppBar(title: Text('$city 天气')),
      body: Center(
        child: asyncWeather.when(
          loading: () => const CircularProgressIndicator(),
          error: (err, stack) => Padding(
    
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  '加载失败: $err',
                  textAlign: TextAlign.center, 
                ),
                const SizedBox(height: 20),
                ElevatedButton(
                  onPressed: () {
                    ref
                        .read(cityControllerProvider.notifier)
                        .updateCity("Beijing");
                    ref.invalidate(weatherControllerProvider(city));
                  },
                  child: const Text('重试'),
                ),
              ],
            ),
          ),
          data: (weather) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                '城市: ${weather.cityName}',
                style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 8),
              Text(
                '天气: ${weather.description}',
                style: const TextStyle(fontSize: 18),
              ),
              const SizedBox(height: 8),
              Text(
                '当前温度:${weather.temperature.toStringAsFixed(1)}°C', 
                style: const TextStyle(fontSize: 24),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

我这里使用的天气库是OpenWeatherMap,我根据OpenWeatherMap的数据编写定义了Weather数据模型以及api服务,大家可以根据自己的需要替换成不同的库来进行测试。效果如下:

由于库本身不支持中文的原因,所以一开始的查询是报错的,当我们点击重试对city重新赋值之后,数据就可以正常显示了。

这个案例给我们完整地展示了从带参初始化状态渲染优雅重试的闭环流程。当我们想要在Riverpod 中实现"刷新"或"重试",ref.invalidate 将会是我们的不二选择。

总结

通过本文的学习,我们掌握了 Riverpod 中处理异步任务的核心武器:

  • AsyncNotifier 统一并极大地简化了异步状态的定义和管理,告别了 FutureProvider + StateNotifier 的冗余组合。
  • AsyncValue 及其 .when 方法为 UI 渲染提供了健壮、清晰的状态处理范式。
  • .autoDispose 机制智能地管理 provider 的生命周期,有效防止资源泄漏。
  • ref.invalidate 提供了简洁而强大的方式来触发 provider 的刷新和重试。

可以说,AsyncNotifier 是 Riverpod 献给 Flutter 开发者处理异步逻辑的最佳礼物。它不仅提升了代码质量,更改善了开发体验。学会驾驭异步,才能真正释放 Riverpod 的力量。坚持实践,我们写下的每一行优秀的代码,都会让我们的 Flutter 应用更优雅、更可靠。

相关推荐
1024小神16 分钟前
使用tauri打包cocos小游戏,并在抖音小玩法中启动,拿到启动参数token
前端
用户游民24 分钟前
Flutter Android 端启动加载流程剖析
前端
林太白34 分钟前
项目中的层级模块到底如何做接口
前端·后端·node.js
lichenyang45337 分钟前
理解虚拟 DOM:前端开发中的高效渲染利器
前端
xiguolangzi1 小时前
vue3 字体管理
前端
Mhua_Z1 小时前
使用 flutter_tts 的配置项
flutter
伍华聪1 小时前
基于Vant4+Vue3+TypeScript的H5移动前端
前端
Nayana1 小时前
axios-取消重复请求--CancelToken&AbortController
前端·axios