0Pricing
Flutter Mobile Development · Урок

Конвейеры данных AsyncNotifier и FutureProvider

Чётко моделируйте состояния загрузки, ошибок и данных с помощью шаблонов AsyncNotifier и AsyncValue.

«Конвейеры данных AsyncNotifier и FutureProvider» — бесплатный урок Flutter Mobile Development на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения 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» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Flutter Mobile Development, подпишись на CoddyKit PRO. Курс Flutter Mobile Development содержит 4 уроков всего.

Чему я научусь в уроке «Конвейеры данных AsyncNotifier и FutureProvider»?

Чётко моделируйте состояния загрузки, ошибок и данных с помощью шаблонов AsyncNotifier и AsyncValue. Ты практикуешь Flutter Mobile Development с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Flutter Mobile Development?

Предыдущий опыт не требуется. Flutter Mobile Development на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 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