Best fit
- 生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。
affaan-m/ECC
生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。
npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/dart-flutter-patterns"Source checked Jul 28, 2026·Refresh due Oct 26, 2026
Reorganized from the pinned upstream SKILL.md
According to the pinned SKILL.md from affaan-m/ECC: 生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。
npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/dart-flutter-patterns"Best fit
Bring this context
Expected outputs
Key source sections
Sections are extracted automatically from the pinned SKILL.md and link back to the source.
开始新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式 审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导 搭建新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间做选择 实现安全的 HTTP 客户端、WebView 集成或本地存储 为 Flutter 组件、Cubit 或 Riverpod 提供者编写测试 使用认证守卫配置 GoRouter
此技能提供按关注点组织的、可直接复制粘贴的 Dart/Flutter 代码模式:
适用于 Dart 和 Flutter 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。
Review the “1. 空安全基础” section in the pinned source before continuing.
Review the “优先使用模式而非感叹号操作符” section in the pinned source before continuing.
SkillSignal prompt templates
These prompts were written by SkillSignal from the source structure; they are not upstream text.
Task-start prompt
Confirm source fit, inputs, and outputs before acting.
Use dart-flutter-patterns to help me with: [specific task]. Context: [files, data, or background]. Constraints: [environment, scope, and prohibited actions]. Before acting, check the pinned SKILL.md and explain which sections apply, what inputs are still missing, and what you will deliver.
Source-guided execution
Make the Agent explicitly follow the key extracted sections.
Apply the pinned dart-flutter-patterns source to [task]. Pay particular attention to these source sections: “使用场景”, “工作原理”, “示例”, “1. 空安全基础”, “优先使用模式而非感叹号操作符”. Preserve the important decision at each step. Mark facts not covered by the source as “needs confirmation” instead of inventing them. Then verify the result against my acceptance criteria: [criteria].
Result-review prompt
Check omissions, permissions, and source drift before delivery.
Review the current dart-flutter-patterns result: (1) does it satisfy the original task; (2) were any applicable steps or limits in the pinned SKILL.md missed; (3) did it perform any unauthorized file, command, network, or data action; and (4) which conclusions remain unverified? List issues first, then fix only what the source or user authorization supports.
Output checklist
The task matches the purpose documented in the SKILL.md.
The source section “使用场景” has been checked.
The source section “工作原理” has been checked.
The source section “示例” has been checked.
The source section “1. 空安全基础” has been checked.
Inputs, constraints, and acceptance criteria are explicit.
Unverified facts, compatibility, and outcome claims are clearly marked.
Any file, command, network, or data action has been reviewed.
Choose a different workflow
Production-ready Dart and Flutter patterns covering null safety, immutable state, async composition, widget architecture, popular state management frameworks (BLoC, Riverpod, Provider), GoRouter navigation, Dio networking, Freezed code generation, and clean architecture.
A separate implementation from affaan-m/ECC; compare its source, maintenance signals, and permission requirements.
Open source detail本番環境対応のDartおよびFlutterパターンは、null安全性、不変状態、非同期構成、ウィジェットアーキテクチャ、人気のある状態管理フレームワーク(BLoC、Riverpod、Provider)、GoRouterナビゲーション、Dioネットワーキング、Freezedコード生成、クリーンアーキテクチャをカバー。
A separate implementation from affaan-m/ECC; compare its source, maintenance signals, and permission requirements.
Open source detailWhen the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program
A separate implementation from coreyhaines31/marketingskills; compare its source, maintenance signals, and permission requirements.
Open source detailFAQ
生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。
The catalog detected this source-specific install command: npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/dart-flutter-patterns". Inspect the command and pinned source before running it.
No dedicated Agent platform is declared in the pinned source record.
Quality breakdown
Based on traceable docs and repository signals; stars are not treated as quality.
Compare before choosing
These links are selected from shared tasks, functions, stacks, platforms, and same-name variants. Compare the source owner, documentation, permissions, and maintenance signals.
Production-ready Dart and Flutter patterns covering null safety, immutable state, async composition, widget architecture, popular state management frameworks (BLoC, Riverpod, Provider), GoRouter navigation, Dio networking, Freezed code generation, and clean architecture.
本番環境対応のDartおよびFlutterパターンは、null安全性、不変状態、非同期構成、ウィジェットアーキテクチャ、人気のある状態管理フレームワーク(BLoC、Riverpod、Provider)、GoRouterナビゲーション、Dioネットワーキング、Freezedコード生成、クリーンアーキテクチャをカバー。
When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program
When the user wants to reduce churn, build cancellation flows, set up save offers, recover failed payments, or implement retention strategies. Also use when the user mentions 'churn,' 'cancel flow,' 'offboarding,' 'save offer,' 'dunning,' 'failed payment recovery,' 'win-back,' 'retention,' 'exit survey,' 'pause subscription,' 'involuntary churn,' 'people keep canceling,' 'churn rate is too high,' 'how do I keep users,' or 'customers are leaving.' Use this whenever someone is losing subscribers o
Grounded design brief from the adopted corpus — style, WCAG-checked color tokens, typography, layout pattern, anti-patterns. Use on ui-design-brief or any which-style/palette/font/chart decision.
在以下情况使用此技能:
此技能提供按关注点组织的、可直接复制粘贴的 Dart/Flutter 代码模式:
!,优先使用 ?./??/模式匹配freezed、copyWithFuture.wait、BuildContext 后安全使用 awaitconst 传播、作用域重建refreshListenable 实现带响应式认证守卫的 GoRouterErrorWidget.builder、Crashlytics 集成// Sealed state — prevents impossible states
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 with reactive auth redirect
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: [...],
);
// Riverpod derived provider with safe firstWhereOrNull
@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 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。
// BAD — crashes at runtime if null
final name = user!.name;
// GOOD — provide fallback
final 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 return
String getUserName(User? user) {
if (user == null) return 'Unknown';
return user.name; // promoted to non-null after check
}
late// BAD — defers null error to runtime
late String userId;
// GOOD — nullable with explicit initialization
String? userId;
// OK — use late only when initialization is guaranteed before first access
// (e.g., in initState() before any widget interaction)
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
}
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 branches
Widget buildFrom(UserState state) => switch (state) {
UserInitial() => const SizedBox.shrink(),
UserLoading() => const CircularProgressIndicator(),
UserLoaded(:final user) => UserCard(user: user),
UserError(:final message) => ErrorText(message),
};
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);
}
// Usage
final user = User(id: '1', name: 'Alice', email: 'alice@example.com');
final updated = user.copyWith(name: 'Alice Smith'); // immutable update
final json = user.toJson();
final fromJson = User.fromJson(json);
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);
}
// Repository exposes reactive streams for live data
Stream<List<Item>> watchCartItems() => _db
.watchTable('cart_items')
.map((rows) => rows.map(Item.fromRow).toList());
// In widget layer — declarative, no manual subscription
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(),
},
)
// CRITICAL — always check mounted after any await in StatefulWidget
Future<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);
}
}
// BAD — private method returning widget, prevents optimization
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(16),
child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
);
}
// GOOD — separate widget class, enables const, element reuse
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),
);
}
}
// BAD — new instances every rebuild
child: Padding(
padding: EdgeInsets.all(16.0), // not const
child: Icon(Icons.home, size: 24.0), // not const
)
// GOOD — const stops rebuild propagation
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.home, size: 24.0),
)
// BAD — entire page rebuilds on every counter change
class 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 part
class 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');
}
}
// Cubit — synchronous or simple async state
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());
}
}
// In 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),
},
)
// Auto-dispose async provider
@riverpod
Future<List<Product>> products(Ref ref) async {
final repo = ref.watch(productRepositoryProvider);
return repo.getAll();
}
// Notifier with complex mutations
@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 = [];
}
// Derived provider (selector pattern)
@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 (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;
});
}
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']!),
),
],
),
],
);
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 interceptor
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 {
// 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 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!);
}
}
// 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 production
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
ErrorWidget.builder = (details) => ProductionErrorWidget(details);
return MaterialApp.router(routerConfig: router);
}
}
// Unit test — use case
test('GetUserUseCase returns null for missing user', () async {
final repo = FakeUserRepository();
final useCase = GetUserUseCase(repo);
expect(await useCase('missing-id'), isNull);
});
// BLoC test
blocTest<AuthCubit, AuthState>(
'emits loading then error on failed login',
build: () => AuthCubit(FakeAuthService(throwsOn: 'login')),
act: (cubit) => cubit.login('user@test.com', 'wrong'),
expect: () => [const AuthState.loading(), isA<AuthError>()],
);
// Widget test
testWidgets('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);
});
flutter-dart-code-review — 全面审查清单rules/dart/ — 编码风格、模式、安全性、测试、钩子