mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-19 23:33:07 +08:00
docs: salvage zh-CN framework skill translations
This commit is contained in:
committed by
Affaan Mustafa
parent
3242ed461f
commit
4359947a6a
565
docs/zh-CN/skills/dart-flutter-patterns/SKILL.md
Normal file
565
docs/zh-CN/skills/dart-flutter-patterns/SKILL.md
Normal file
@@ -0,0 +1,565 @@
|
||||
---
|
||||
name: dart-flutter-patterns
|
||||
description: 生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、流行的状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# 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 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 应用程序的实用、生产就绪模式。尽可能保持库无关性,并明确覆盖最常见的生态系统包。
|
||||
|
||||
***
|
||||
|
||||
## 1. 空安全基础
|
||||
|
||||
### 优先使用模式而非感叹号操作符
|
||||
|
||||
```dart
|
||||
// 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`
|
||||
|
||||
```dart
|
||||
// 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));
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 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 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),
|
||||
};
|
||||
```
|
||||
|
||||
### 使用 Freezed 实现无模板代码的不可变性
|
||||
|
||||
```dart
|
||||
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);
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 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 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(),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Await 后的 BuildContext
|
||||
|
||||
```dart
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 4. 组件架构
|
||||
|
||||
### 提取为类,而非方法
|
||||
|
||||
```dart
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### const 传播
|
||||
|
||||
```dart
|
||||
// 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),
|
||||
)
|
||||
```
|
||||
|
||||
### 作用域重建
|
||||
|
||||
```dart
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 5. 状态管理:BLoC/Cubit
|
||||
|
||||
```dart
|
||||
// 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),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 6. 状态管理:Riverpod
|
||||
|
||||
```dart
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 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 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!);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 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 production
|
||||
class App extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ErrorWidget.builder = (details) => ProductionErrorWidget(details);
|
||||
return MaterialApp.router(routerConfig: router);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 10. 测试快速参考
|
||||
|
||||
```dart
|
||||
// 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);
|
||||
});
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 参考
|
||||
|
||||
* [Effective Dart: 设计](https://dart.dev/effective-dart/design)
|
||||
* [Flutter 性能最佳实践](https://docs.flutter.dev/perf/best-practices)
|
||||
* [Riverpod 文档](https://riverpod.dev/)
|
||||
* [BLoC 库](https://bloclibrary.dev/)
|
||||
* [GoRouter](https://pub.dev/packages/go_router)
|
||||
* [Freezed](https://pub.dev/packages/freezed)
|
||||
* 技能:`flutter-dart-code-review` — 全面审查清单
|
||||
* 规则:`rules/dart/` — 编码风格、模式、安全性、测试、钩子
|
||||
Reference in New Issue
Block a user