🧪 Skills

Riverpod

Flutter state management with Riverpod - declarative, type-safe, and code-generated providers. Use when building Flutter apps that need reactive state manage...

v1.0.0
❤️ 0
⬇️ 105
👁 1
Share

Description


name: riverpod description: Flutter state management with Riverpod - declarative, type-safe, and code-generated providers. Use when building Flutter apps that need reactive state management, dependency injection, and testable business logic. Covers ProviderNotifier, AsyncNotifier, StreamProvider, Family modifiers, and code generation with @riverpod.

Riverpod State Management

Riverpod is a declarative, type-safe state management solution for Flutter. It uses code generation for boilerplate reduction.

Core Concepts

1. Declarative State with @riverpod

Mark classes or functions with @riverpod for code generation:

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
}

2. Consuming State

Use ConsumerWidget or ConsumerStatefulWidget:

class CounterView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

3. Provider Types

Type Use For Syntax
StateProvider Simple state @riverpod class X extends _$X
AsyncNotifier Async loading @riverpod class X extends _$X with Future
StreamProvider Real-time data @riverpod Stream<T> func(Ref ref)
Family providers Parameterized @riverpod class X extends Family<X, Args>

Code Patterns

Pattern 1: AsyncNotifier for API Calls

@riverpod
class UserController extends _$UserController {
  @override
  Future<User> build(String userId) async {
    final dio = ref.watch(dioProvider);
    final response = await dio.get('/users/$userId');
    return User.fromJson(response.data);
  }

  Future<void> updateUser(User user) async {
    // Optimistic update
    final previous = await future;
    state = AsyncData(user);

    try {
      await ref.read(dioProvider).put('/users/${user.id}', user.toJson());
    } catch (e) {
      state = AsyncError(e, StackTrace.current);
    }
  }
}

Pattern 2: Combining Providers

@riverpod
List<Todo> filteredTodos(Ref ref) {
  final todos = ref.watch(todosProvider);
  final filter = ref.watch(filterProvider);

  return switch (filter) {
    Filter.all => todos,
    Filter.completed => todos.where((t) => t.completed).toList(),
    Filter.uncompleted => todos.where((t) => !t.completed).toList(),
  };
}

Pattern 3: Dependency Injection

@riverpod
Dio dio(Ref ref) {
  final baseUrl = ref.watch(baseUrlProvider);
  return Dio(BaseOptions(baseUrl: baseUrl));
}

Pattern 4: AsyncValue Pattern Matching

class AsyncWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final data = ref.watch(asyncProvider);

    return switch (data) {
      AsyncData(:final value) => Text('$value'),
      AsyncError(:final error, :final stackTrace) => ErrorWidget(error),
      AsyncLoading() => const CircularProgressIndicator(),
    };
  }
}

Provider Modifiers

Family - Parameterized Providers

@riverpod
Future<User> user(UserRef ref, String userId) async {
  return await api.getUser(userId);
}

// Usage
ref.watch(userProvider('123'));

AutoDispose - Automatic Cleanup

@Riverpod(keepAlive: false)
Future<User> temporaryUser(TemporaryUserRef ref, String id) async {
  ref.onDispose(() {
    // Cleanup logic
  });
  return await api.getUser(id);
}

Widget Patterns

ConsumerWidget Pattern

class MyWidget extends ConsumerWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(myProvider);
    return Text('$value');
  }
}

ConsumerStatefulWidget Pattern

class MyPage extends ConsumerStatefulWidget {
  const MyPage({super.key});

  @override
  ConsumerState<MyPage> createState() => _MyPageState();
}

class _MyPageState extends ConsumerState<MyPage> {
  @override
  void initState() {
    super.initState();
    ref.read(myProvider.notifier).load();
  }

  @override
  Widget build(BuildContext context) {
    final state = ref.watch(myProvider);
    return Scaffold(body: Text('$state'));
  }
}

HookConsumerWidget (with flutter_hooks)

class HookWidget extends HookConsumerWidget {
  const HookWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final controller = useTextController();
    final count = ref.watch(counterProvider);
    
    return TextField(controller: controller);
  }
}

Code Generation Workflow

  1. Add annotation: @riverpod or @Riverpod()
  2. Define class extending _$ClassName or function
  3. Run: flutter pub run build_runner watch --delete-conflicting-outputs
  4. Generated file: .g.dart extension

Essential Commands

# Generate once
flutter pub run build_runner build --delete-conflicting-outputs

# Watch for changes (recommended during development)
flutter pub run build_runner watch --delete-conflicting-outputs

Testing

testWidgets('counter increments', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      child: MaterialApp(home: CounterView()),
    ),
  );

  expect(find.text('0'), findsOneWidget);
});

ref Methods

Method Use For
ref.watch(provider) Rebuild when value changes
ref.read(provider) One-time read, no rebuild
ref.listen(provider, cb) Side effects on change
ref.refresh(provider) Force reload
ref.invalidate(provider) Mark as needing refresh

Best Practices

See BEST_PRACTICES.md for detailed guidelines on:

  • Provider architecture
  • Avoiding common pitfalls
  • Performance optimization
  • Testing strategies

Reviews (0)

Sign in to write a review.

No reviews yet. Be the first to review!

Comments (0)

Sign in to join the discussion.

No comments yet. Be the first to share your thoughts!

Compatible Platforms

Pricing

Free

Related Configs