0Pricing
Flutter Mobile Development · 课时

AsyncNotifier 与 FutureProvider 数据流水线

使用 AsyncNotifier 和 AsyncValue 模式,清晰地建模加载、错误和数据状态。

AsyncNotifier 与 FutureProvider 数据流水线 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

The Three States of Async Data

Every screen that loads data from the network must answer three questions: Is it loading? Did it fail? What is the data?

Riverpod 2.0 models these three states with a single sealed type called AsyncValue<T>. Instead of juggling separate isLoading, error, and data fields by hand, you receive one value that is always in exactly one of three shapes:

  • AsyncLoading<T> — the future is still running
  • AsyncError<T> — it threw, carrying the error and stack trace
  • AsyncData<T> — it completed successfully, carrying the value

Both FutureProvider and AsyncNotifier expose their state as an AsyncValue, which is why the UI code for either looks identical.

FutureProvider: the simplest pipeline

FutureProvider is the lightest way to run an async computation and expose its result. You give it an async callback; Riverpod runs it, caches the result, and rebuilds dependents as the state moves from loading to data or error.

Use it when the data is read-only and has no side-effect methods — for example, fetching a user profile once and displaying it.

final userProfileProvider = FutureProvider<UserProfile>((ref) async {
  final repo = ref.watch(profileRepositoryProvider);
  return repo.fetchProfile();
});

// In a ConsumerWidget
Widget build(BuildContext context, WidgetRef ref) {
  final profile = ref.watch(userProfileProvider);
  return profile.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, st) => Text('Failed: $err'),
    data: (p) => Text('Hello, ${p.name}'),
  );
}

Pattern-matching with .when

The .when method forces you to handle all three cases, so you can never forget a loading spinner or an error message. It is the idiomatic way to render an AsyncValue in the UI.

Two extra flags are worth knowing:

  • skipLoadingOnRefresh — keep showing old data while a refresh runs in the background instead of flashing a spinner.
  • skipLoadingOnReload — same idea when a dependency rebuilds the provider.

For richer control you can also pattern-match directly on the sealed subclasses using Dart 3 switch expressions.

final state = ref.watch(userProfileProvider);

final widget = switch (state) {
  AsyncData(:final value) => Text('Hello, ${value.name}'),
  AsyncError(:final error) => Text('Error: $error'),
  _ => const CircularProgressIndicator(),
};

When you need methods: AsyncNotifier

FutureProvider can only read. The moment you need to mutate state — refresh, add, toggle, delete — you reach for AsyncNotifier.

An AsyncNotifier<T> has a build() method that returns a Future<T> for the initial load, plus your own public methods that update state. The class holds business logic; the widget just calls its methods.

Key rule: build() is the initial pipeline. Riverpod automatically wraps its result so state starts as AsyncLoading, then becomes AsyncData or AsyncError — you never set those manually for the initial load.

class TodosNotifier extends AsyncNotifier<List<Todo>> {
  @override
  Future<List<Todo>> build() async {
    final repo = ref.watch(todoRepositoryProvider);
    return repo.fetchTodos();
  }
}

final todosProvider =
    AsyncNotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);

Mutating with AsyncValue.guard

Inside a mutation method you must set state = const AsyncLoading() before the async work, then capture either data or error afterward. Writing that try/catch by hand is repetitive and easy to get wrong.

AsyncValue.guard does it for you: it runs an async callback and returns AsyncData on success or AsyncError (with the stack trace) on throw. This keeps the loading and error states consistent automatically.

Future<void> addTodo(String label) async {
  final repo = ref.read(todoRepositoryProvider);

  // Show a spinner while the write happens.
  state = const AsyncValue.loading();

  state = await AsyncValue.guard(() async {
    await repo.create(label);
    return repo.fetchTodos(); // fresh list becomes the new state
  });
}

Optimistic updates with the current value

Flashing a full-screen spinner on every tap feels janky. A better mobile UX keeps the existing list visible and only swaps in the new data when it arrives.

The trick is to read state.value (the last known data) and pass it to AsyncLoading via copyWithPrevious, or simply build the next list optimistically. Riverpod's requireValue gives you the data or throws if there is none yet.

Future<void> toggle(String id) async {
  final previous = state.requireValue;

  // Optimistic: update UI immediately.
  final optimistic = [
    for (final t in previous)
      if (t.id == id) t.copyWith(done: !t.done) else t,
  ];
  state = AsyncData(optimistic);

  // Reconcile with the server; revert on failure.
  state = await AsyncValue.guard(() async {
    await ref.read(todoRepositoryProvider).toggle(id);
    return ref.read(todoRepositoryProvider).fetchTodos();
  });
}

Refreshing and invalidating

To re-run a provider's pipeline from the UI — say on pull-to-refresh — you do not call a method on the notifier. You ask Riverpod to rebuild the provider:

  • ref.invalidate(provider) — discards the cached value; build() runs again next read.
  • ref.refresh(provider) — same, but returns the new value/future so you can await it.

Because build() re-runs, the state automatically cycles back through AsyncLoading then AsyncData. Pair this with skipLoadingOnRefresh: false if you want a spinner during refresh.

RefreshIndicator(
  onRefresh: () => ref.refresh(todosProvider.future),
  child: ref.watch(todosProvider).when(
    loading: () => const Center(child: CircularProgressIndicator()),
    error: (e, st) => ErrorView(error: e),
    data: (todos) => TodoListView(todos: todos),
  ),
);

Chaining providers into a pipeline

Real data flows are pipelines: an auth token feeds a user id, which feeds a list of orders. With Riverpod you express this by watch-ing one async provider inside another.

When you await ref.watch(other.future), the outer provider waits for the inner one's data, and if the inner provider re-emits, the outer pipeline automatically re-runs. This composes AsyncValues without manual chaining.

final ordersProvider = FutureProvider<List<Order>>((ref) async {
  // Wait for the upstream user before fetching their orders.
  final user = await ref.watch(userProfileProvider.future);
  final repo = ref.watch(orderRepositoryProvider);
  return repo.fetchOrdersFor(user.id);
});

Parameterized pipelines with .family

Often the pipeline depends on an argument — a product id, a search query. The .family modifier creates a distinct provider instance per argument, each with its own cached AsyncValue.

Combine it with autoDispose so instances are freed when no widget watches them — important on mobile to avoid leaking memory across navigation.

final productProvider = FutureProvider.autoDispose
    .family<Product, String>((ref, id) async {
  final repo = ref.watch(catalogRepositoryProvider);
  return repo.fetchProduct(id);
});

// Usage: each id gets its own loading/error/data cache.
final product = ref.watch(productProvider('sku-42'));

Modeling AsyncValue without Flutter

To really understand the three-state model, it helps to see it as plain Dart. Below is a tiny sealed-class reimplementation that mirrors how AsyncValue behaves — loading, error, and data — and a when-style fold over it.

This is the exact mental model Riverpod uses under the hood; the real type just adds caching, previous-value tracking, and stack traces.

sealed class Async<T> {
  const Async();
}

class Loading<T> extends Async<T> { const Loading(); }
class Failure<T> extends Async<T> {
  final Object error;
  const Failure(this.error);
}
class Data<T> extends Async<T> {
  final T value;
  const Data(this.value);
}

String render(Async<int> s) => switch (s) {
  Loading() => 'loading...',
  Failure(:final error) => 'error: $error',
  Data(:final value) => 'data: $value',
};

void main() {
  print(render(const Loading()));
  print(render(const Failure('timeout')));
  print(render(const Data(42)));
}

Avoiding common pipeline pitfalls

A few rules keep your AsyncNotifier pipelines clean:

  • Never use ref.read for dependencies inside build() — use ref.watch so the pipeline rebuilds when they change. Use ref.read only inside one-shot mutation methods.
  • Do not catch and swallow errors in build() — let them throw so the state becomes AsyncError and the UI can show it.
  • Return the future, not void, from refresh handlers so pull-to-refresh awaits completion.
  • Keep build() idempotent — it can run many times; avoid side effects there.

Quick Check: Read vs Mutate

You are building a todo screen. The list loads from an API and users can add new todos. Which provider choice is correct?

Recap

You now have the full async data pipeline toolkit in Riverpod 2.0:

  • AsyncValue<T> models loading / error / data as one sealed value, rendered with .when or a switch.
  • FutureProvider is for read-only async data with no methods.
  • AsyncNotifier adds mutation methods; build() is the initial pipeline and must use ref.watch for dependencies.
  • AsyncValue.guard wraps mutations so loading and error states stay consistent; requireValue enables optimistic updates.
  • ref.invalidate / ref.refresh re-run the pipeline; .family and autoDispose handle parameterized, memory-safe pipelines.

Reach for FutureProvider when you only read, and AsyncNotifier the moment you need to write.

常见问题解答

「AsyncNotifier 与 FutureProvider 数据流水线」课时是免费的吗?

是的 — 「AsyncNotifier 与 FutureProvider 数据流水线」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。

「AsyncNotifier 与 FutureProvider 数据流水线」这节课中我会学到什么?

使用 AsyncNotifier 和 AsyncValue 模式,清晰地建模加载、错误和数据状态。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Flutter Mobile Development 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「AsyncNotifier 与 FutureProvider 数据流水线」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Flutter Mobile Development 课中编写并运行代码吗?

能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 从 Provider 到 Riverpod:迁移旧状态管理
  2. 使用 riverpod_generator 和 @riverpod 生成代码
  3. AsyncNotifier 与 FutureProvider 数据流水线
  4. 提供者作用域、覆盖与 ProviderObserver
← 返回 Flutter Mobile Development