Dart Flutter Patterns

affaan-m/ECC/docs/zh-CN/skills/dart-flutter-patterns

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775无许可证275K 个星标收录于 2026年10月9日更新于 2026年10月9日仓库4天前更新

生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。

AI 生成的概览

面向生产环境的 Dart 与 Flutter 模式参考,涵盖状态、导航、网络与测试。

功能
该技能按关注点提供可直接复制粘贴的 Dart 与 Flutter 模式:空安全、使用密封类与 Freezed 的不可变状态、异步组合、组件架构、BLoC/Cubit 与 Riverpod 状态管理、带认证守卫的 GoRouter 导航、带令牌刷新的 Dio HTTP 客户端、错误处理以及测试。它是一份带代码示例的指导文档,而不是生成文件的工具。文中还列出了外部参考资料以及相关技能和规则。
适用场景
在开始新的 Flutter 功能、需要状态管理、导航或数据访问的惯用模式时使用;在审查或编写涉及空安全、密封类型或异步组合的 Dart 代码时使用;在搭建项目并在 BLoC、Riverpod 与 Provider 之间做选择时使用。
运行要求
无需脚本或运行时,仅为说明与代码示例。示例涉及 freezed、riverpod、bloc、go_router、dio、collection 等包以及 Crashlytics 等集成,使用者的项目需自行添加。

Dart/Flutter 模式

使用场景

在以下情况使用此技能:

  • 开始新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式
  • 审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导
  • 搭建新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间做选择
  • 实现安全的 HTTP 客户端、WebView 集成或本地存储
  • 为 Flutter 组件、Cubit 或 Riverpod 提供者编写测试
  • 使用认证守卫配置 GoRouter

工作原理

此技能提供按关注点组织的、可直接复制粘贴的 Dart/Flutter 代码模式:

  1. 空安全 — 避免 !,优先使用 ?./??/模式匹配
  2. 不可变状态 — 密封类、freezed、copyWith
  3. 异步组合 — 并发 Future.wait、BuildContext 后安全使用 await
  4. 组件架构 — 提取为类(而非方法)、const 传播、作用域重建
  5. 状态管理 — BLoC/Cubit 事件、Riverpod 通知器和派生提供者
  6. 导航 — 通过 refreshListenable 实现带响应式认证守卫的 GoRouter
  7. 网络请求 — 带拦截器的 Dio、带一次性重试守卫的令牌刷新
  8. 错误处理 — 全局捕获、ErrorWidget.builder、Crashlytics 集成
  9. 测试 — 单元测试(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);});

参考

来源与署名

来源:affaan-m/ECC位于docs/zh-CN/skills/dart-flutter-patterns提交ef648e0

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架