Help us improve
Share bugs, ideas, or general feedback.
Provides production-ready Dart and Flutter code patterns for null safety, sealed classes, async composition, widget architecture, state management (BLoC, Riverpod, Provider), GoRouter navigation, Dio HTTP, Freezed codegen, and clean architecture.
npx claudepluginhub aaione/everything-claude-code-zhHow this skill is triggered — by the user, by Claude, or both
Slash command
/everything-claude-code:dart-flutter-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
在以下情况使用此技能:
Provides production-ready Dart/Flutter patterns for null safety, immutable state, async composition, BLoC/Riverpod/Provider state management, GoRouter navigation, Dio networking, Freezed code gen, testing, and clean architecture.
Provides expert Flutter/Dart patterns for cross-platform mobile apps including feature-first project structure, const widget best practices, and Riverpod/Bloc state management.
Builds cross-platform Flutter 3+ apps with Dart: widget development, Riverpod/Bloc state management, GoRouter navigation, platform-specific code, testing, and performance optimization.
Share bugs, ideas, or general feedback.
在以下情况使用此技能:
此技能提供可直接复制粘贴的 Dart/Flutter 代码模式,按关注点组织:
!,优先使用 ?./??/模式匹配freezed、copyWithFuture.wait、await 后安全的 BuildContextconst 传播、作用域重建refreshListenable 实现响应式认证守卫的 GoRouterErrorWidget.builder、crashlytics 集成// Sealed 状态 — 防止不可能的状态
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: [...],
);
// Riverpod 派生 provider,使用安全的 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 应用的实用、生产就绪模式。尽可能与库无关,同时明确覆盖最常用的生态系统包。
// 差 — 如果为 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// 差 — 将 null 错误延迟到运行时
late String userId;
// 好 — 可空类型配合显式初始化
String? userId;
// 可以 — 仅当初始化保证在首次访问之前时使用 late
// (例如在 initState() 中,任何 widget 交互之前)
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;
}
// 穷尽 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),
};
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: 'alice@example.com');
final updated = user.copyWith(name: 'Alice Smith'); // 不可变更新
final json = user.toJson();
final fromJson = User.fromJson(json);
Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async {
// 并发运行 — 不要顺序等待
final (userList, orderList) = await (
users.getAll(),
orders.getRecent(),
).wait; // Dart 3 record 解构 + Future.wait 扩展
return DashboardData(users: userList, orders: orderList);
}
// Repository 暴露响应式 stream 用于实时数据
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(),
},
)
// 关键 — 在 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);
}
}
// 差 — 返回 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),
);
}
}
// 差 — 每次重建都创建新实例
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');
}
}
// 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),
},
)
// 自动释放的异步 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 包)避免 product 缺失时的 StateError
final product = products.firstWhereOrNull((p) => p.id == item.productId);
return total + (product?.price ?? 0) * item.quantity;
});
}
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']!),
),
],
),
],
);
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 的 Repository
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!);
}
}
// 全局错误捕获 — 在 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 {
@override
Widget build(BuildContext context) {
ErrorWidget.builder = (details) => ProductionErrorWidget(details);
return MaterialApp.router(routerConfig: router);
}
}
// 单元测试 — 用例
test('GetUserUseCase 对缺失用户返回 null', () async {
final repo = FakeUserRepository();
final useCase = GetUserUseCase(repo);
expect(await useCase('missing-id'), isNull);
});
// BLoC 测试
blocTest<AuthCubit, AuthState>(
'登录失败时发出 loading 然后 error',
build: () => AuthCubit(FakeAuthService(throwsOn: 'login')),
act: (cubit) => cubit.login('user@test.com', 'wrong'),
expect: () => [const AuthState.loading(), isA<AuthError>()],
);
// Widget 测试
testWidgets('CartBadge 显示商品数量', (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/ — 编码风格、模式、安全、测试、钩子