正在加载,请稍候…

Flutter State Management with Riverpod 2: Architecture and Testing Patterns

Master Flutter state management with Riverpod 2. Code-generated providers, AsyncNotifier, dependency injection with repository pattern, and comprehensive testing strategies.

Riverpod 2 with code generation solves Flutter state management cleanly. Providers are immutable declarations; the framework handles lifecycle, caching, and disposal.

Setup

# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.5.0
  riverpod_annotation: ^2.3.0
dev_dependencies:
  build_runner: ^2.4.0
  riverpod_generator: ^2.4.0

Code-Generated Providers

import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';

// Synchronous provider
@riverpod
String appVersion(AppVersionRef ref) => '2.0.0';

// Async provider (auto-handles loading/error states)
@riverpod
Future<User> user(UserRef ref, String userId) async {
  return ref.watch(userRepositoryProvider).getById(userId);
}

// Keep-alive: survive widget disposal
@Riverpod(keepAlive: true)
AppConfig appConfig(AppConfigRef ref) => AppConfig.fromEnv();

AsyncNotifier for Complex State

@riverpod
class ProductList extends _$ProductList {
  @override
  Future<List<Product>> build() async {
    return ref.read(productRepositoryProvider).getProducts();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(
      () => ref.read(productRepositoryProvider).getProducts()
    );
  }

  Future<void> loadMore() async {
    final current = state.valueOrNull ?? [];
    final more = await ref.read(productRepositoryProvider)
        .getProducts(page: (current.length ~/ 20) + 1);
    state = AsyncData([...current, ...more]);
  }

  Future<void> delete(String id) async {
    await ref.read(productRepositoryProvider).delete(id);
    state = state.whenData(
      (products) => products.where((p) => p.id != id).toList(),
    );
  }
}

ConsumerWidget Usage

class ProductScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productsState = ref.watch(productListProvider);

    return productsState.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(
        child: Column(children: [
          Text('Error: $error'),
          ElevatedButton(
            onPressed: () => ref.invalidate(productListProvider),
            child: const Text('Retry'),
          ),
        ]),
      ),
      data: (products) => ListView.builder(
        itemCount: products.length,
        itemBuilder: (_, i) => ProductTile(product: products[i]),
      ),
    );
  }
}

Repository Pattern + Dependency Injection

abstract class ProductRepository {
  Future<List<Product>> getProducts({int page = 1});
  Future<void> delete(String id);
}

class HttpProductRepository implements ProductRepository {
  HttpProductRepository(this._client);
  final http.Client _client;

  @override
  Future<List<Product>> getProducts({int page = 1}) async {
    final resp = await _client.get(Uri.parse('/api/products?page=$page'));
    if (resp.statusCode != 200) throw ApiException(resp);
    return (jsonDecode(resp.body)['data'] as List).map(Product.fromJson).toList();
  }
}

@riverpod
ProductRepository productRepository(ProductRepositoryRef ref) =>
    HttpProductRepository(ref.watch(httpClientProvider));

// Override for testing
ProviderScope(
  overrides: [productRepositoryProvider.overrideWithValue(MockProductRepository())],
  child: const MyApp(),
)

Testing

test('ProductList deletes item', () async {
  final mock = MockProductRepository();
  when(mock.getProducts()).thenAnswer((_) async => [
    Product(id: '1', name: 'Item A'),
    Product(id: '2', name: 'Item B'),
  ]);

  final container = ProviderContainer(overrides: [
    productRepositoryProvider.overrideWithValue(mock),
  ]);
  addTearDown(container.dispose);

  await container.read(productListProvider.future);
  await container.read(productListProvider.notifier).delete('1');

  final products = container.read(productListProvider).valueOrNull!;
  expect(products.length, equals(1));
  expect(products.first.id, equals('2'));
});

Riverpod code generation removes the manual type juggling. Providers are functions; state is immutable; testing is straightforward.

→ Analyze your API responses with the JSON Viewer tool.