Files
worker/RIVERPOD_SETUP.md
Phuoc Nguyen 628c81ce13 runable
2025-10-17 17:22:28 +07:00

14 KiB

Riverpod 3.0 Setup - Worker Flutter App

Overview

This document provides a complete guide to the Riverpod 3.0 state management setup for the Worker Flutter app.

What's Configured

1. Dependencies (pubspec.yaml)

Production Dependencies:

  • flutter_riverpod: ^3.0.0 - Main Riverpod package
  • riverpod_annotation: ^3.0.0 - Annotations for code generation

Development Dependencies:

  • build_runner: ^2.4.11 - Code generation runner
  • riverpod_generator: ^3.0.0 - Generates provider code from annotations
  • riverpod_lint: ^3.0.0 - Riverpod-specific linting rules
  • custom_lint: ^0.7.0 - Required for riverpod_lint

2. Build Configuration (build.yaml)

Configured to generate code for:

  • **_provider.dart files
  • Files in **/providers/ directories
  • Files in **/notifiers/ directories

3. Analysis Options (analysis_options.yaml)

Configured with:

  • Custom lint plugin enabled
  • Exclusion of generated files (*.g.dart, *.freezed.dart)
  • Riverpod-specific lint rules
  • Comprehensive code quality rules

4. App Initialization (main.dart)

Wrapped with ProviderScope:

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

Directory Structure

lib/core/providers/
├── connectivity_provider.dart    # Network connectivity monitoring
├── provider_examples.dart        # Comprehensive Riverpod 3.0 examples
└── README.md                     # Provider architecture documentation

Quick Start

1. Install Dependencies

flutter pub get

2. Generate Provider Code

# One-time generation
dart run build_runner build --delete-conflicting-outputs

# Watch mode (auto-regenerates on file changes)
dart run build_runner watch -d

3. Use the Setup Script

chmod +x scripts/setup_riverpod.sh
./scripts/setup_riverpod.sh

Core Providers

Connectivity Provider

Location: /lib/core/providers/connectivity_provider.dart

Purpose: Monitor network connectivity status across the app.

Providers Available:

  1. connectivityProvider - Connectivity instance

    final connectivity = ref.watch(connectivityProvider);
    
  2. connectivityStreamProvider - Real-time connectivity stream

    final status = ref.watch(connectivityStreamProvider);
    status.when(
      data: (status) => Text('Status: $status'),
      loading: () => CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
    );
    
  3. currentConnectivityProvider - One-time connectivity check

    final status = await ref.read(currentConnectivityProvider.future);
    
  4. isOnlineProvider - Boolean online/offline stream

    final isOnline = ref.watch(isOnlineProvider);
    

Usage Example:

class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final connectivityState = ref.watch(connectivityStreamProvider);

    return connectivityState.when(
      data: (status) {
        if (status == ConnectivityStatus.offline) {
          return OfflineBanner();
        }
        return OnlineContent();
      },
      loading: () => LoadingIndicator(),
      error: (error, _) => ErrorWidget(error),
    );
  }
}

Riverpod 3.0 Key Features

1. @riverpod Annotation (Code Generation)

The modern, recommended approach:

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'my_provider.g.dart';

// Simple value
@riverpod
String greeting(GreetingRef ref) => 'Hello';

// Async value
@riverpod
Future<User> user(UserRef ref, String id) async {
  return await fetchUser(id);
}

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

  void increment() => state++;
}

2. Unified Ref Type

No more separate FutureProviderRef, StreamProviderRef, etc. - just Ref:

@riverpod
Future<String> example(ExampleRef ref) async {
  ref.watch(provider1);
  ref.read(provider2);
  ref.listen(provider3, (prev, next) {});
}

3. Family as Function Parameters

// Simple parameter
@riverpod
Future<User> user(UserRef ref, String id) async {
  return await fetchUser(id);
}

// Multiple parameters with named, optional, defaults
@riverpod
Future<List<Post>> posts(
  PostsRef ref, {
  required String userId,
  int page = 1,
  int limit = 20,
  String? category,
}) async {
  return await fetchPosts(userId, page, limit, category);
}

// Usage
ref.watch(userProvider('user123'));
ref.watch(postsProvider(userId: 'user123', page: 2));

4. AutoDispose vs KeepAlive

// AutoDispose (default) - cleaned up when not watched
@riverpod
String autoExample(AutoExampleRef ref) => 'Auto disposed';

// KeepAlive - stays alive until app closes
@Riverpod(keepAlive: true)
String keepExample(KeepExampleRef ref) => 'Kept alive';

5. ref.mounted Check

New in Riverpod 3.0 - check if provider is still alive after async operations:

@riverpod
class DataManager extends _$DataManager {
  @override
  String build() => 'Initial';

  Future<void> updateData() async {
    await Future.delayed(Duration(seconds: 2));

    // Check if provider is still mounted
    if (!ref.mounted) return;

    state = 'Updated';
  }
}

6. AsyncValue.guard() for Error Handling

@riverpod
class UserProfile extends _$UserProfile {
  @override
  Future<User> build() async => await fetchUser();

  Future<void> update(String name) async {
    state = const AsyncValue.loading();

    // AsyncValue.guard catches errors automatically
    state = await AsyncValue.guard(() async {
      return await updateUser(name);
    });
  }
}

Provider Patterns

1. Simple Provider (Immutable Value)

@riverpod
String appVersion(AppVersionRef ref) => '1.0.0';

@riverpod
int pointsMultiplier(PointsMultiplierRef ref) {
  final tier = ref.watch(userTierProvider);
  return tier == 'diamond' ? 3 : 2;
}

2. FutureProvider (Async Data)

@riverpod
Future<User> currentUser(CurrentUserRef ref) async {
  final token = await ref.watch(authTokenProvider.future);
  return await fetchUser(token);
}

3. StreamProvider (Real-time Data)

@riverpod
Stream<List<Message>> chatMessages(ChatMessagesRef ref, String roomId) {
  return ref.watch(webSocketProvider).messages(roomId);
}

4. Notifier (Mutable State)

@riverpod
class Cart extends _$Cart {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    state = [...state, CartItem.fromProduct(product)];
  }

  void removeItem(String id) {
    state = state.where((item) => item.id != id).toList();
  }

  void clear() {
    state = [];
  }
}

5. AsyncNotifier (Async Mutable State)

@riverpod
class UserProfile extends _$UserProfile {
  @override
  Future<User> build() async {
    return await ref.read(userRepositoryProvider).getCurrentUser();
  }

  Future<void> updateName(String name) async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(() async {
      final updated = await ref.read(userRepositoryProvider).updateName(name);
      return updated;
    });
  }

  Future<void> refresh() async {
    ref.invalidateSelf();
  }
}

6. StreamNotifier (Stream Mutable State)

@riverpod
class LiveChat extends _$LiveChat {
  @override
  Stream<List<Message>> build(String roomId) {
    return ref.watch(chatServiceProvider).messagesStream(roomId);
  }

  Future<void> sendMessage(String text) async {
    await ref.read(chatServiceProvider).send(roomId, text);
  }
}

Usage in Widgets

ConsumerWidget

class ProductList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final products = ref.watch(productsProvider);

    return products.when(
      data: (list) => ListView.builder(
        itemCount: list.length,
        itemBuilder: (context, index) => ProductCard(list[index]),
      ),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => ErrorView(error),
    );
  }
}

ConsumerStatefulWidget

class OrderPage extends ConsumerStatefulWidget {
  @override
  ConsumerState<OrderPage> createState() => _OrderPageState();
}

class _OrderPageState extends ConsumerState<OrderPage> {
  @override
  void initState() {
    super.initState();
    // Can use ref in all lifecycle methods
    Future.microtask(
      () => ref.read(ordersProvider.notifier).loadOrders(),
    );
  }

  @override
  Widget build(BuildContext context) {
    final orders = ref.watch(ordersProvider);
    return OrderList(orders);
  }
}

Consumer (Optimization)

Column(
  children: [
    const StaticHeader(),
    Consumer(
      builder: (context, ref, child) {
        final count = ref.watch(cartCountProvider);
        return CartBadge(count);
      },
    ),
  ],
)

Performance Optimization

Use .select() to Watch Specific Fields

// Bad - rebuilds on any user change
final user = ref.watch(userProvider);

// Good - rebuilds only when name changes
final name = ref.watch(userProvider.select((user) => user.name));

// Good with AsyncValue
final userName = ref.watch(
  userProfileProvider.select((async) => async.value?.name),
);

Provider Composition

@riverpod
Future<Dashboard> dashboard(DashboardRef ref) async {
  // Depend on other providers
  final user = await ref.watch(userProvider.future);
  final stats = await ref.watch(statsProvider.future);
  final orders = await ref.watch(recentOrdersProvider.future);

  return Dashboard(
    user: user,
    stats: stats,
    recentOrders: orders,
  );
}

Testing

Unit Testing Providers

test('counter increments', () {
  final container = ProviderContainer();
  addTearDown(container.dispose);

  expect(container.read(counterProvider), 0);
  container.read(counterProvider.notifier).increment();
  expect(container.read(counterProvider), 1);
});

test('async provider fetches data', () async {
  final container = ProviderContainer();
  addTearDown(container.dispose);

  final user = await container.read(userProvider.future);
  expect(user.name, 'John Doe');
});

Widget Testing

testWidgets('displays user name', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        userProvider.overrideWith((ref) => User(name: 'Test User')),
      ],
      child: MaterialApp(home: UserScreen()),
    ),
  );

  expect(find.text('Test User'), findsOneWidget);
});

Mocking Providers

testWidgets('handles loading state', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        userProvider.overrideWith((ref) {
          return Future.delayed(
            Duration(seconds: 10),
            () => User(name: 'Test'),
          );
        }),
      ],
      child: MaterialApp(home: UserScreen()),
    ),
  );

  expect(find.byType(CircularProgressIndicator), findsOneWidget);
});

Linting

Run Riverpod Lint

# Check for Riverpod-specific issues
dart run custom_lint

# Auto-fix issues
dart run custom_lint --fix

Riverpod Lint Rules Enabled

  • provider_dependencies - Ensure proper dependency usage
  • scoped_providers_should_specify_dependencies - Scoped provider safety
  • avoid_public_notifier_properties - Encapsulation
  • avoid_ref_read_inside_build - Performance (don't use ref.read in build)
  • avoid_manual_providers_as_generated_provider_dependency - Use generated providers
  • functional_ref - Proper ref usage
  • notifier_build - Proper Notifier implementation

Common Issues & Solutions

Issue 1: Generated files not found

Solution:

dart run build_runner build --delete-conflicting-outputs

Issue 2: Provider not updating

Solution: Check if you're using ref.watch() not ref.read() in build method.

Issue 3: Memory leaks

Solution: Use autoDispose (default) for providers that should clean up. Only use keepAlive for global state.

Issue 4: Too many rebuilds

Solution: Use .select() to watch specific fields instead of entire objects.

Migration from Riverpod 2.x

StateNotifierProvider → Notifier

// Old (2.x)
class Counter extends StateNotifier<int> {
  Counter() : super(0);
  void increment() => state++;
}
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

// New (3.0)
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;
  void increment() => state++;
}

Provider.family → Function Parameters

// Old (2.x)
final userProvider = FutureProvider.family<User, String>((ref, id) async {
  return fetchUser(id);
});

// New (3.0)
@riverpod
Future<User> user(UserRef ref, String id) async {
  return fetchUser(id);
}

Examples

Comprehensive examples are available in:

  • /lib/core/providers/provider_examples.dart - All Riverpod 3.0 patterns
  • /lib/core/providers/connectivity_provider.dart - Real-world connectivity monitoring

Resources

Next Steps

  1. Run flutter pub get to install dependencies
  2. Run dart run build_runner watch -d to start code generation
  3. Create feature-specific providers in lib/features/*/presentation/providers/
  4. Follow the patterns in provider_examples.dart
  5. Use connectivity_provider as a reference for real-world implementation

Support

For questions or issues:

  1. Check provider_examples.dart for patterns
  2. Review the Riverpod documentation
  3. Run custom_lint to catch common mistakes
  4. Use ref.watch() in build methods, ref.read() in event handlers