0Pricing
Flutter Mobile Development · 강의

AsyncNotifier 및 FutureProvider 데이터 파이프라인

AsyncNotifier와 AsyncValue 패턴으로 로딩, 오류 및 데이터 상태를 깔끔하게 모델링합니다.

AsyncNotifier 및 FutureProvider 데이터 파이프라인은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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 데이터 파이프라인” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

“AsyncNotifier 및 FutureProvider 데이터 파이프라인”에서 뭘 배우나요?

AsyncNotifier와 AsyncValue 패턴으로 로딩, 오류 및 데이터 상태를 깔끔하게 모델링합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“AsyncNotifier 및 FutureProvider 데이터 파이프라인” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Provider에서 Riverpod로: 레거시 상태 마이그레이션
  2. riverpod_generator 및 @riverpod를 활용한 코드 생성
  3. AsyncNotifier 및 FutureProvider 데이터 파이프라인
  4. Provider 범위 지정, 재정의 및 ProviderObserver
← Flutter Mobile Development(으)로 돌아가기