正在加载,请稍候…

Flutter 状态管理:使用 Riverpod 2 的架构与测试模式

掌握使用 Riverpod 2 进行 Flutter 状态管理,包括代码生成提供者、AsyncNotifier、结合仓库模式的依赖注入以及全面的测试策略。

Flutter State Management with Riverpod 2: Architecture and Testing Patterns

Riverpod 2 结合代码生成,简洁地解决了 Flutter 状态管理问题。提供者是不可变声明;框架负责生命周期、缓存和销毁。

设置

# 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

Flutter State Management with Riverpod 2: Architecture and Testing Patterns illustration

代码生成的提供者

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

// 同步提供者
@riverpod
String appVersion(AppVersionRef ref) => '2.0.0';

// 异步提供者(自动处理加载/错误状态)
@riverpod
Future<User> user(UserRef ref, String userId) async {
  return ref.watch(userRepositoryProvider).getById(userId);
}

// 保持存活:在组件销毁后仍然存在
@Riverpod(keepAlive: true)
AppConfig appConfig(AppConfigRef ref) => AppConfig.fromEnv();

Flutter State Management with Riverpod 2: Architecture and Testing Patterns illustration

用于复杂状态的 AsyncNotifier

@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(),
    );
  }
}

Flutter State Management with Riverpod 2: Architecture and Testing Patterns illustration

ConsumerWidget 的使用

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]),
      ),
    );
  }
}

仓库模式 + 依赖注入

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));

// 测试时覆盖
ProviderScope(
  overrides: [productRepositoryProvider.overrideWithValue(MockProductRepository())],
  child: const MyApp(),
)

测试

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 代码生成消除了手动类型转换。提供者是函数;状态是不可变的;测试变得简单直接。

→ 使用 JSON Viewer 工具分析你的 API 响应。