flutter · 3 min read

Structure a Flutter Riverpod application

By Vivek Patel · Updated

Riverpod is most effective when providers describe dependencies instead of becoming an unstructured global namespace. Place feature providers beside the feature, keep shared service providers near the composition root, and use modern Notifier APIs for mutable state.

How the pieces connect

Consumer widget→ Notifier provider→ Repository provider→ Data source

An illustrative folder structure

This structure explains the architecture. Exact generated paths are shown in the source walkthrough below.

lib/
  features/catalog/
    presentation/catalog_page.dart
    state/catalog_provider.dart
    domain/catalog_repository.dart
    data/api_catalog_repository.dart
  core/dependency_injection/container.dart

Separate reading state from performing actions

A Consumer widget watches a provider to rebuild when state changes. Event handlers read the notifier and invoke an action. Do not trigger repeated side effects from a build method. Keep derived values in providers when multiple consumers need the same calculation.

Make lifetime an explicit decision

An automatically disposed search provider is useful for temporary screens. A session provider may need a longer lifetime. Persistence belongs in a repository or storage service, not in the assumption that a provider will remain mounted. Test navigation away and back before relying on retained state.

Design async states around the screen

Use AsyncNotifier for asynchronous state ownership. Distinguish initial loading from refreshing existing data, and expose actionable failures. Invalidate only the relevant dependencies after a write. Broad invalidation can trigger unnecessary requests across unrelated screens.

Test with isolated provider containers

Override a repository provider with a fake, create an isolated container, and dispose it after each test. Assert the initial state, the action result, and failure behavior. For widgets, wrap the screen in a ProviderScope with the same overrides. Generated code is optional; handwritten NotifierProvider declarations avoid a build step.

Inside the generated starter

Start from the exact Notifier emitted by AppScaffold and make its repository dependency independently replaceable. This is a useful first extension before adding remote data or caching.

These source files are produced from the same templates used by the generator, with the configuration below. They are starter examples; the observations identify work still needed for a real feature.

lib/features/home/state/counter_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/dependency_injection/container.dart';

final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;
  Future<void> increment() async { final next = state + 1; await container.counterRepository.save(next); state = next; }
}

lib/domain/repositories/counter_repository.dart

abstract interface class CounterRepository {
  Future<int> load();
  Future<void> save(int value);
}

What to notice

The generated provider is handwritten, so this counter does not need build_runner. The state begins at zero; it does not restore a value by calling load during initialization.

The notifier accesses container.counterRepository directly. For a provider-based dependency graph, create a Provider<CounterRepository> and use ref.read inside the action. This creates an override point for isolated tests and makes the dependency visible to Riverpod.

The starter uses Notifier<int>, not AsyncNotifier. If initial state comes from a database or network, model asynchronous initialization explicitly and decide what the screen displays while loading. A provider surviving navigation is not a persistence guarantee.

Extend it and verify the result

Replace direct container access with a repository provider. Override it with an in-memory fake in a fresh ProviderContainer for each test, and dispose the container afterward. Next add initial loading and verify what happens when a screen is reopened after the repository changes.

  • Separate test containers do not share counter state.
  • A fake save failure produces an intentional UI state.
  • Initial loading does not trigger a second request on every rebuild.
  • Invalidating the counter does not reset unrelated session state.

These are exercises to add to your application tests, not claims that the starter already handles every case.

Try this configuration

Clean Architecture · Riverpod · Dio · Riverpod DI

The generator includes a working counter example with a repository contract, selected dependencies, navigation, and local setup instructions. Extend the example around your own domain before shipping.

Configure your Flutter project →

For prerequisites, exact setup commands, expected behavior, and build troubleshooting, follow the flutter download-to-first-run walkthrough. Check the validation record for the configurations actually checked.

Continue reading

Primary reference

Riverpod testing providers. The source walkthrough above describes AppScaffold’s implementation; platform documentation describes the broader APIs and recommendations.