Communitygithub.com

dart-flutter-patterns

涵盖空安全、不可变状态、异步组合、Widget架构、流行状态管理框架(BLoC、Riverpod、Provider)、GoRouter导航、Dio网络请求、Freezed代码生成和清洁架构的生产级Dart和Flutter模式。在编写或审查Dart和Flutter代码(状态、Widget、导航、网络请求或架构)时使用。

dart-flutter-patterns 是什么?

这是一个针对Dart和Flutter开发者的全面技能,提供了生产级代码模式和最佳实践。它涵盖了从空安全和不可变状态的基础知识到使用BLoC、Riverpod和Provider进行状态管理、GoRouter导航、Dio网络请求以及Freezed代码生成等高级主题。该技能在SKILL.md中组织成清晰的章节,每个章节都包含立即可用的代码示例和"反面/正面"模式的对比,以促进惯用的Dart/Flutter开发。它支持所有主要平台,包括Android、iOS、Web和桌面,适用于新功能开发、代码审查和项目搭建。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/dart-flutter-patterns

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Dart/Flutter 模式

何时使用

在以下情况使用此技能:

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

工作原理

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

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

示例

// 密封状态 — 防止不可能的状态
sealed 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
final 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: [...],
);

// 带安全 firstWhereOrNull 的 Riverpod 派生 Provider
@riverpod
double 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. 空安全基础

优先使用模式而非感叹号操作符

// 不良实践 — 如果为 null,运行时会崩溃
final name = user!.name;

// 良好实践 — 提供后备值
final name = user?.name ?? 'Unknown';

// 良好实践 — Dart 3 模式匹配(复杂情况优先选择)
final display = switch (user) {
  User(:final name, :final email) => '$name <$email>',
  null => 'Guest',
};

// 良好实践 — 提前返回守卫
String getUserName(User? user) {
  if (user == null) return 'Unknown';
  return user.name; // 检查后提升为非空
}

避免过度使用 late

// 不良实践 — 将空错误推迟到运行时
late String userId;

// 良好实践 — 使用可空类型并显式初始化
String? userId;

// 可以 — 仅在首次访问前保证初始化时使用 late
// (例如,在任何 Widget 交互之前的 initState() 中)
late final AnimationController _controller;

@override
void initState() {
  super.initState();
  _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
}

2. 不可变状态

用于状态层次结构的密封类

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

// 穷举 switch — 编译器强制执行所有分支
Widget buildFrom(UserState state) => switch (state) {
  UserInitial() => const SizedBox.shrink(),
  UserLoading() => const CircularProgressIndicator(),
  UserLoaded(:final user) => UserCard(user: user),
  UserError(:final message) => ErrorText(message),
};

使用 Freezed 实现零模板代码的不可变性

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class 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);
}

// 用法
final user = User(id: '1', name: 'Alice', email: '[email protected]');
final updated = user.copyWith(name: 'Alice Smith'); // 不可变更新
final json = user.toJson();
final fromJson = User.fromJson(json);

3. 异步组合

使用 Future.wait 的结构化并发

Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async {
  // 并发运行 — 不要顺序 await
  final (userList, orderList) = await (
    users.getAll(),
    orders.getRecent(),
  ).wait; // Dart 3 记录解构 + Future.wait 扩展

  return DashboardData(users: userList, orders: orderList);
}

流模式

// 仓库为实时数据暴露响应式流
Stream<List<Item>> watchCartItems() => _db
    .watchTable('cart_items')
    .map((rows) => rows.map(Item.fromRow).toList());

// 在 Widget 层 — 声明式,无需手动订阅
StreamBuilder<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

// 关键 — 在 StatefulWidget 中的任何 await 之后始终检查 mounted
Future<void> _handleSubmit() async {
  setState(() => _isLoading = true);
  try {
    await authService.login(_email, _password);
    if (!mounted) return; // ← 在使用 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. Widget 架构

提取为类,而非方法

// 不良实践 — 返回 Widget 的私有方法,阻止优化
Widget _buildHeader() {
  return Container(
    padding: const EdgeInsets.all(16),
    child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
  );
}

// 良好实践 — 单独的 Widget 类,启用 const、元素复用
class _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 传播

// 不良实践 — 每次重建都创建新实例
child: Padding(
  padding: EdgeInsets.all(16.0),       // 不是 const
  child: Icon(Icons.home, size: 24.0), // 不是 const
)

// 良好实践 — const 阻止重建传播
child: const Padding(
  padding: EdgeInsets.all(16.0),
  child: Icon(Icons.home, size: 24.0),
)

作用域重建

// 不良实践 — 每次计数变化时整个页面重建
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider); // 重建所有内容
    return Scaffold(
      body: Column(children: [
        const ExpensiveHeader(), // 不必要地重建
        Text('$count'),
        const ExpensiveFooter(), // 不必要地重建
      ]),
    );
  }
}

// 良好实践 — 隔离重建部分
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Column(children: [
        ExpensiveHeader(),        // 从不重建(const)
        _CounterDisplay(),        // 仅此部分重建
        ExpensiveFooter(),        // 从不重建(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

// Cubit — 同步或简单异步状态
class 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());
  }
}

// 在 Widget 中
BlocBuilder<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

// 自动释放异步 Provider
@riverpod
Future<List<Product>> products(Ref ref) async {
  final repo = ref.watch(productRepositoryProvider);
  return repo.getAll();
}

// 带复杂变更的 Notifier
@riverpod
class 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 = [];
}

// 派生 Provider(选择器模式)
@riverpod
int cartCount(Ref ref) => ref.watch(cartNotifierProvider).length;

@riverpod
double cartTotal(Ref ref) {
  final cart = ref.watch(cartNotifierProvider);
  final products = ref.watch(productsProvider).valueOrNull ?? [];
  return cart.fold(0.0, (total, item) {
    // firstWhereOrNull(来自 collection 包)在缺少产品时避免 StateError
    final product = products.firstWhereOrNull((p) => p.id == item.productId);
    return total + (product?.price ?? 0) * item.quantity;
  });
}

7. 使用 GoRouter 导航

final router = GoRouter(
  initialLocation: '/',
  // refreshListenable 在身份验证状态变化时重新评估重定向
  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 请求

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

// 添加身份验证拦截器
dio.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 {
    // 防止无限重试循环:每个请求仅尝试刷新一次
    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);
  },
));

// 使用 Dio 的仓库
class 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. 错误处理架构

// 全局错误捕获 — 在 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());
}

// 自定义生产环境的 ErrorWidget
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      builder: (context, widget) {
        ErrorWidget.builder = (details) => const _AppErrorScreen();
        return widget!;
      },
      routerConfig: router,
    );
  }
}

// 全局 Zone 错误捕获
runZonedGuarded(() {
  runApp(const App());
}, (error, stackTrace) {
  crashlytics.recordError(error, stackTrace, fatal: true);
});

10. 测试模式

单元测试 BLoC/Cubit

void main() {
  group('AuthCubit', () {
    late MockAuthService mockService;
    late AuthCubit cubit;

    setUp(() {
      mockService = MockAuthService();
      cubit = AuthCubit(mockService);
    });

    blocTest<AuthCubit, AuthState>(
      'emits [loading, authenticated] on success',
      build: () => cubit,
      act: (cubit) => cubit.login('[email protected]', 'pass'),
      setUp: () => when(() => mockService.login(any(), any()))
          .thenAnswer((_) async => User(id: '1', name: 'Alice', email: '[email protected]')),
      expect: () => [
        const AuthState.loading(),
        AuthState.authenticated(User(id: '1', name: 'Alice', email: '[email protected]')),
      ],
    );
  });
}

Widget 测试中的 Provider 覆盖

void main() {
  testWidgets('CartPage shows total for multiple items', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          cartNotifierProvider.overrideWith(() => _TestCartNotifier()),
        ],
        child: const MaterialApp(home: CartPage()),
      ),
    );

    expect(find.text('\$30.00'), findsOneWidget);
  });
}

class _TestCartNotifier extends CartNotifier {
  @override
  List<CartItem> build() => [
    CartItem(productId: '1', quantity: 2),
    CartItem(productId: '2', quantity: 1),
  ];
}

关键原则总结

原理不良实践良好实践
空安全user!.nameuser?.name ?? 'Unknown'
不可变性可变类 + settercopyWith、密封类、Freezed
Widget返回 Widget 的方法单独的 const Widget 类
状态管理在 Widget 内存储状态BLoC/Cubit、Riverpod Provider
导航Navigator.pushNamedGoRouter + 身份验证守卫
网络请求http.get 直接使用带拦截器的 Dio
异步顺序 await并发的 Future.wait
错误处理try/catch 在所有地方全局 + 用于 crash 报告的 Crashlytics

为 Dart 和 Flutter 应用提供实用、生产级的模式。在可能的情况下保持库无关,并明确覆盖最常见的生态系统包。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相关技能