Dart/Flutter 模式
使用场景
在以下情况使用此技能:
- 开始新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式
- 审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导
- 搭建新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间做选择
- 实现安全的 HTTP 客户端、WebView 集成或本地存储
- 为 Flutter 组件、Cubit 或 Riverpod 提供者编写测试
- 使用认证守卫配置 GoRouter
工作原理
此技能提供按关注点组织的、可直接复制粘贴的 Dart/Flutter 代码模式:
- 空安全 — 避免
!,优先使用?./??/模式匹配 - 不可变状态 — 密封类、
freezed、copyWith - 异步组合 — 并发
Future.wait、BuildContext后安全使用await - 组件架构 — 提取为类(而非方法)、
const传播、作用域重建 - 状态管理 — BLoC/Cubit 事件、Riverpod 通知器和派生提供者
- 导航 — 通过
refreshListenable实现带响应式认证守卫的 GoRouter - 网络请求 — 带拦截器的 Dio、带一次性重试守卫的令牌刷新
- 错误处理 — 全局捕获、
ErrorWidget.builder、Crashlytics 集成 - 测试 — 单元测试(BLoC 测试)、组件测试(ProviderScope 覆盖)、使用假对象而非模拟对象
示例
dart
// Sealed state — prevents impossible statessealed class AsyncState<T> {}final class Loading<T> extends AsyncState<T> {}final class Success<T> extends AsyncState<T> { final T data; const Success(this.data); }final class Failure<T> extends AsyncState<T> { final Object error; const Failure(this.error); }
// GoRouter with reactive auth redirectfinal router = GoRouter( refreshListenable: GoRouterRefreshStream(authCubit.stream), redirect: (context, state) { final authed = context.read<AuthCubit>().state is AuthAuthenticated; if (!authed && !state.matchedLocation.startsWith('/login')) return '/login'; return null; }, routes: [...],);
// Riverpod derived provider with safe firstWhereOrNull@riverpoddouble cartTotal(Ref ref) { final cart = ref.watch(cartNotifierProvider); final products = ref.watch(productsProvider).valueOrNull ?? []; return cart.fold(0.0, (total, item) { final product = products.firstWhereOrNull((p) => p.id == item.productId); return total + (product?.price ?? 0) * item.quantity; });}适用于 Dart 和 Flutter 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。
1. 空安全基础
优先使用模式而非感叹号操作符
dart
// BAD — crashes at runtime if nullfinal name = user!.name;
// GOOD — provide fallbackfinal name = user?.name ?? 'Unknown';
// GOOD — Dart 3 pattern matching (preferred for complex cases)final display = switch (user) { User(:final name, :final email) => '$name <$email>', null => 'Guest',};
// GOOD — guard early returnString getUserName(User? user) { if (user == null) return 'Unknown'; return user.name; // promoted to non-null after check}避免过度使用 late
dart
// BAD — defers null error to runtimelate String userId;
// GOOD — nullable with explicit initializationString? userId;
// OK — use late only when initialization is guaranteed before first access// (e.g., in initState() before any widget interaction)late final AnimationController _controller;
@overridevoid initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));}2. 不可变状态
状态层次结构的密封类
dart
sealed class UserState {}
final class UserInitial extends UserState {}
final class UserLoading extends UserState {}
final class UserLoaded extends UserState { const UserLoaded(this.user); final User user;}
final class UserError extends UserState { const UserError(this.message); final String message;}
// Exhaustive switch — compiler enforces all branchesWidget buildFrom(UserState state) => switch (state) { UserInitial() => const SizedBox.shrink(), UserLoading() => const CircularProgressIndicator(), UserLoaded(:final user) => UserCard(user: user), UserError(:final message) => ErrorText(message),};使用 Freezed 实现无模板代码的不可变性
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';part 'user.g.dart';
@freezedclass User with _$User { const factory User({ required String id, required String name, required String email, @Default(false) bool isAdmin, }) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);}
// Usagefinal user = User(id: '1', name: 'Alice', email: '[email protected]');final updated = user.copyWith(name: 'Alice Smith'); // immutable updatefinal json = user.toJson();final fromJson = User.fromJson(json);3. 异步组合
使用 Future.wait 的结构化并发
dart
Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async { // Run concurrently — don't await sequentially final (userList, orderList) = await ( users.getAll(), orders.getRecent(), ).wait; // Dart 3 record destructuring + Future.wait extension
return DashboardData(users: userList, orders: orderList);}流模式
dart
// Repository exposes reactive streams for live dataStream<List<Item>> watchCartItems() => _db .watchTable('cart_items') .map((rows) => rows.map(Item.fromRow).toList());
// In widget layer — declarative, no manual subscriptionStreamBuilder<List<Item>>( stream: cartRepository.watchCartItems(), builder: (context, snapshot) => switch (snapshot) { AsyncSnapshot(connectionState: ConnectionState.waiting) => const CircularProgressIndicator(), AsyncSnapshot(:final error?) => ErrorWidget(error.toString()), AsyncSnapshot(:final data?) => CartList(items: data), _ => const SizedBox.shrink(), },)Await 后的 BuildContext
dart
// CRITICAL — always check mounted after any await in StatefulWidgetFuture<void> _handleSubmit() async { setState(() => _isLoading = true); try { await authService.login(_email, _password); if (!mounted) return; // ← guard before using context context.go('/home'); } on AuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message))); } finally { if (mounted) setState(() => _isLoading = false); }}4. 组件架构
提取为类,而非方法
dart
// BAD — private method returning widget, prevents optimizationWidget _buildHeader() { return Container( padding: const EdgeInsets.all(16), child: Text(title, style: Theme.of(context).textTheme.headlineMedium), );}
// GOOD — separate widget class, enables const, element reuseclass _PageHeader extends StatelessWidget { const _PageHeader(this.title); final String title;
@override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), child: Text(title, style: Theme.of(context).textTheme.headlineMedium), ); }}const 传播
dart
// BAD — new instances every rebuildchild: Padding( padding: EdgeInsets.all(16.0), // not const child: Icon(Icons.home, size: 24.0), // not const)
// GOOD — const stops rebuild propagationchild: const Padding( padding: EdgeInsets.all(16.0), child: Icon(Icons.home, size: 24.0),)作用域重建
dart
// BAD — entire page rebuilds on every counter changeclass CounterPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); // rebuilds everything return Scaffold( body: Column(children: [ const ExpensiveHeader(), // unnecessarily rebuilt Text('$count'), const ExpensiveFooter(), // unnecessarily rebuilt ]), ); }}
// GOOD — isolate the rebuilding partclass CounterPage extends StatelessWidget { const CounterPage({super.key});
@override Widget build(BuildContext context) { return const Scaffold( body: Column(children: [ ExpensiveHeader(), // never rebuilt (const) _CounterDisplay(), // only this rebuilds ExpensiveFooter(), // never rebuilt (const) ]), ); }}
class _CounterDisplay extends ConsumerWidget { const _CounterDisplay();
@override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return Text('$count'); }}5. 状态管理:BLoC/Cubit
dart
// Cubit — synchronous or simple async stateclass AuthCubit extends Cubit<AuthState> { AuthCubit(this._authService) : super(const AuthState.initial()); final AuthService _authService;
Future<void> login(String email, String password) async { emit(const AuthState.loading()); try { final user = await _authService.login(email, password); emit(AuthState.authenticated(user)); } on AuthException catch (e) { emit(AuthState.error(e.message)); } }
void logout() { _authService.logout(); emit(const AuthState.initial()); }}
// In widgetBlocBuilder<AuthCubit, AuthState>( builder: (context, state) => switch (state) { AuthInitial() => const LoginForm(), AuthLoading() => const CircularProgressIndicator(), AuthAuthenticated(:final user) => HomePage(user: user), AuthError(:final message) => ErrorView(message: message), },)6. 状态管理:Riverpod
dart
// Auto-dispose async provider@riverpodFuture<List<Product>> products(Ref ref) async { final repo = ref.watch(productRepositoryProvider); return repo.getAll();}
// Notifier with complex mutations@riverpodclass CartNotifier extends _$CartNotifier { @override List<CartItem> build() => [];
void add(Product product) { final existing = state.where((i) => i.productId == product.id).firstOrNull; if (existing != null) { state = [ for (final item in state) if (item.productId == product.id) item.copyWith(quantity: item.quantity + 1) else item, ]; } else { state = [...state, CartItem(productId: product.id, quantity: 1)]; } }
void remove(String productId) => state = state.where((i) => i.productId != productId).toList();
void clear() => state = [];}
// Derived provider (selector pattern)@riverpodint cartCount(Ref ref) => ref.watch(cartNotifierProvider).length;
@riverpoddouble cartTotal(Ref ref) { final cart = ref.watch(cartNotifierProvider); final products = ref.watch(productsProvider).valueOrNull ?? []; return cart.fold(0.0, (total, item) { // firstWhereOrNull (from collection package) avoids StateError when product is missing final product = products.firstWhereOrNull((p) => p.id == item.productId); return total + (product?.price ?? 0) * item.quantity; });}7. 使用 GoRouter 的导航
dart
final router = GoRouter( initialLocation: '/', // refreshListenable re-evaluates redirect whenever auth state changes refreshListenable: GoRouterRefreshStream(authCubit.stream), redirect: (context, state) { final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated; final isGoingToLogin = state.matchedLocation == '/login'; if (!isLoggedIn && !isGoingToLogin) return '/login'; if (isLoggedIn && isGoingToLogin) return '/'; return null; }, routes: [ GoRoute(path: '/login', builder: (_, __) => const LoginPage()), ShellRoute( builder: (context, state, child) => AppShell(child: child), routes: [ GoRoute(path: '/', builder: (_, __) => const HomePage()), GoRoute( path: '/products/:id', builder: (context, state) => ProductDetailPage(id: state.pathParameters['id']!), ), ], ), ],);8. 使用 Dio 的 HTTP 请求
dart
final dio = Dio(BaseOptions( baseUrl: const String.fromEnvironment('API_URL'), connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), headers: {'Content-Type': 'application/json'},));
// Add auth interceptordio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { final token = await secureStorage.read(key: 'auth_token'); if (token != null) options.headers['Authorization'] = 'Bearer $token'; handler.next(options); }, onError: (error, handler) async { // Guard against infinite retry loops: only attempt refresh once per request final isRetry = error.requestOptions.extra['_isRetry'] == true; if (!isRetry && error.response?.statusCode == 401) { final refreshed = await attemptTokenRefresh(); if (refreshed) { error.requestOptions.extra['_isRetry'] = true; return handler.resolve(await dio.fetch(error.requestOptions)); } } handler.next(error); },));
// Repository using Dioclass UserApiDataSource { const UserApiDataSource(this._dio); final Dio _dio;
Future<User> getById(String id) async { final response = await _dio.get<Map<String, dynamic>>('/users/$id'); return User.fromJson(response.data!); }}9. 错误处理架构
dart
// Global error capture — set up in main()void main() { FlutterError.onError = (details) { FlutterError.presentError(details); crashlytics.recordFlutterFatalError(details); };
PlatformDispatcher.instance.onError = (error, stack) { crashlytics.recordError(error, stack, fatal: true); return true; };
runApp(const App());}
// Custom ErrorWidget for productionclass App extends StatelessWidget { @override Widget build(BuildContext context) { ErrorWidget.builder = (details) => ProductionErrorWidget(details); return MaterialApp.router(routerConfig: router); }}10. 测试快速参考
dart
// Unit test — use casetest('GetUserUseCase returns null for missing user', () async { final repo = FakeUserRepository(); final useCase = GetUserUseCase(repo); expect(await useCase('missing-id'), isNull);});
// BLoC testblocTest<AuthCubit, AuthState>( 'emits loading then error on failed login', build: () => AuthCubit(FakeAuthService(throwsOn: 'login')), act: (cubit) => cubit.login('[email protected]', 'wrong'), expect: () => [const AuthState.loading(), isA<AuthError>()],);
// Widget testtestWidgets('CartBadge shows item count', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier(count: 3))], child: const MaterialApp(home: CartBadge()), ), ); expect(find.text('3'), findsOneWidget);});参考
- Effective Dart: 设计
- Flutter 性能最佳实践
- Riverpod 文档
- BLoC 库
- GoRouter
- Freezed
- 技能:
flutter-dart-code-review— 全面审查清单 - 规则:
rules/dart/— 编码风格、模式、安全性、测试、钩子


