Flutter 开发技能
加载于:base.md
项目结构
project/
├── lib/
│ ├── core/ # 核心工具
│ │ ├── constants/ # 应用常量
│ │ ├── extensions/ # Dart 扩展
│ │ ├── router/ # go_router 配置
│ │ │ └── app_router.dart
│ │ └── theme/ # 应用主题
│ │ └── app_theme.dart
│ ├── data/ # 数据层
│ │ ├── models/ # Freezed 数据模型
│ │ ├── repositories/ # 仓库实现
│ │ └── services/ # API 服务
│ ├── domain/ # 领域层
│ │ ├── entities/ # 商业实体
│ │ └── repositories/ # 仓库接口
│ ├── presentation/ # UI层
│ │ ├── common/ # 共享组件
│ │ ├── features/ # 功能模块
│ │ │ └── feature_name/
│ │ │ ├── providers/ # Riverpod 提供者
│ │ │ ├── widgets/ # 特定功能组件
│ │ │ └── feature_screen.dart
│ │ └── providers/ # 全局提供者
│ ├── main.dart
│ └── app.dart
├── test/
│ ├── unit/ # 单元测试
│ ├── widget/ # 组件测试
│ └── integration/ # 集成测试
├── pubspec.yaml
├── analysis_options.yaml
└── CLAUDE.md
Riverpod 状态管理
提供者类型
// 简单值提供者
final appNameProvider = Provider<String>((ref) => '我的应用');
// StateProvider 用于简单可变状态
final counterProvider = StateProvider<int>((ref) => 0);
// NotifierProvider 用于复杂状态逻辑
final userProvider = NotifierProvider<UserNotifier, User?>(() => UserNotifier());
// AsyncNotifierProvider 用于异步操作
final usersProvider = AsyncNotifierProvider<UsersNotifier, List<User>>(
() => UsersNotifier(),
);
// FutureProvider 用于简单异步数据
final configProvider = FutureProvider<Config>((ref) async {
return await ref.watch(configServiceProvider).loadConfig();
});
// StreamProvider 用于实时数据
final messagesProvider = StreamProvider<List<Message>>((ref) {
return ref.watch(messageServiceProvider).watchMessages();
});
// Family 提供者用于参数化数据
final userByIdProvider = FutureProvider.family<User, String>((ref, userId) async {
return await ref.watch(userRepositoryProvider).getUser(userId);
});
通知者模式
@riverpod
class Users extends _$Users {
@override
Future<List<User>> build() async {
return await _fetchUsers();
}
Future<List<User>> _fetchUsers() async {
final repository = ref.read(userRepositoryProvider);
return await repository.getUsers();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => _fetchUsers());
}
Future<void> addUser(User user) async {
final repository = ref.read(userRepositoryProvider);
await repository.addUser(user);
ref.invalidateSelf();
}
}
AsyncValue 处理
class UsersScreen extends ConsumerWidget {
const UsersScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return usersAsync.when(
data: (users) => UsersList(users: users),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorDisplay(
error: error,
onRetry: () => ref.invalidate(usersProvider),
),
);
}
}
// 模式匹配替代方案
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return switch (usersAsync) {
AsyncData(:final value) => UsersList(users: value),
AsyncLoading() => const LoadingIndicator(),
AsyncError(:final error) => ErrorDisplay(error: error),
};
}
ref 方法
// watch - 当提供者变化时重建
final users = ref.watch(usersProvider);
// read - 一次性读取,不重建
void onButtonPressed() {
ref.read(counterProvider.notifier).state++;
}
// listen - 反应变化而不重建
ref.listen(authProvider, (previous, next) {
if (next == null) {
context.go('/login');
}
});
// invalidate - 强制刷新
ref.invalidate(usersProvider);
// keepAlive - 防止自动释放
final link = ref.keepAlive();
// 稍后:link.close() 允许释放
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 isActive,
DateTime? createdAt,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// 联合类型用于状态
@freezed
sealed class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.loading() = _Loading;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.unauthenticated() = _Unauthenticated;
const factory AuthState.error(String message) = _Error;
}
使用 Freezed 联合
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
return authState.when(
initial: () => const SplashScreen(),
loading: () => const LoadingScreen(),
authenticated: (user) => HomeScreen(user: user),
unauthenticated: () => const LoginScreen(),
error: (message) => ErrorScreen(message: message),
);
}
go_router 导航
路由器配置
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authProvider);
return GoRouter(
initialLocation: '/',
refreshListenable: authState,
redirect: (context, state) {
final isLoggedIn = authState.valueOrNull != null;
final isLoggingIn = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn && isLoggingIn) return '/';
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'user/:id',
builder: (context, state) => UserScreen(
userId: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
],
errorBuilder: (context, state) => ErrorScreen(error: state.error),
);
});
导航
// 跳转到路由
context.go('/user/123');
// 推到堆栈
context.push('/user/123');
// 弹出当前路由
context.pop();
// 替换当前路由
context.pushReplacement('/home');
// 命名路由
context.goNamed('user', pathParameters: {'id': '123'});
组件模式
ConsumerWidget vs ConsumerStatefulWidget
// 无状态与 Riverpod
class UserCard extends ConsumerWidget {
const UserCard({super.key, required this.userId});
final String userId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userByIdProvider(userId));
return user.when(
data: (user) => Card(child: Text(user.name)),
loading: () => const CardSkeleton(),
error: (e, _) => ErrorCard(error: e),
);
}
}
// 有状态与 Riverpod
class SearchScreen extends ConsumerStatefulWidget {
const SearchScreen({super.key});
@override
ConsumerState<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends ConsumerState<SearchScreen> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final results = ref.watch(searchProvider(_controller.text));
return Column(
children: [
TextField(
controller: _controller,
onChanged: (_) => setState(() {}),
),
Expanded(child: SearchResults(results: results)),
],
);
}
}
HookConsumerWidget (with flutter_hooks)
class AnimatedCounter extends HookConsumerWidget {
const AnimatedCounter({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = useAnimationController(duration: const Duration(milliseconds: 300));
final count = ref.watch(counterProvider);
useEffect(() {
controller.forward(from: 0);
return null;
}, [count]);
return ScaleTransition(
scale: controller,
child: Text('$count'),
);
}
}
测试与 Mocktail
单元测试
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:riverpod/riverpod.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository mockRepository;
late ProviderContainer container;
setUp(() {
mockRepository = MockUserRepository();
container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(mockRepository),
],
);
});
tearDown(() {
container.dispose();
});
test('usersProvider 返回用户列表', () async {
final users = [User(id: '1', name: 'John', email: 'john@example.com')];
when(() => mockRepository.getUsers()).thenAnswer((_) async => users);
final result = await container.read(usersProvider.future);
expect(result, equals(users));
verify(() => mockRepository.getUsers()).called(1);
});
}
组件测试
void main() {
testWidgets('UserCard 显示用户名', (tester) async {
final user = User(id: '1', name: 'John', email: 'john@example.com');
await tester.pumpWidget(
ProviderScope(
overrides: [
userByIdProvider('1').overrideWith((_) => AsyncData(user)),
],
child: const MaterialApp(home: UserCard(userId: '1')),
),
);
expect(find.text('John'), findsOneWidget);
});
testWidgets('UserCard 显示加载指示器', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userByIdProvider('1').overrideWith((_) => const AsyncLoading()),
],
child: const MaterialApp(home: UserCard(userId: '1')),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
}
pubspec.yaml
name: my_app
description: 一个 Flutter 应用
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.2.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# 状态管理
flutter_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3
# 数据模型
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
# 导航
go_router: ^13.0.0
# 网络
dio: ^5.4.0
# 存储
shared_preferences: ^2.2.2
# 工具
intl: ^0.19.0
dev_dependencies:
flutter_test:
sdk: flutter
# 代码生成
build_runner: ^2.4.8
freezed: ^2.4.6
json_serializable: ^6.7.1
riverpod_generator: ^2.3.9
# 测试
mocktail: ^1.0.2
# 代码检查
flutter_lints: ^3.0.1
GitHub Actions
name: Flutter CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
channel: 'stable'
cache: true
- name: 安装依赖
run: flutter pub get
- name: 生成代码
run: dart run build_runner build --delete-conflicting-outputs
- name: 分析
run: flutter analyze --fatal-infos
- name: 运行测试
run: flutter test --coverage
- name: 构建 APK
run: flutter build apk --release
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
errors:
invalid_annotation_target: ignore
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
linter:
rules:
- always_declare_return_types
- avoid_dynamic_calls
- avoid_print
- avoid_type_to_string
- cancel_subscriptions
- close_sinks
- prefer_const_constructors
- prefer_const_declarations
- prefer_final_locals
- require_trailing_commas
- unawaited_futures
- use_super_parameters
Flutter 反模式
- ❌ 没有 autoDispose 的 Provider - 使用
.autoDispose防止内存泄漏 - ❌ 在回调中 watch - 在 onPressed/回调中使用
ref.read(),而不是ref.watch() - ❌ 业务逻辑在组件中 - 移动到 Notifiers/providers
- ❌ 在提供者中使用可变状态 - 使用 Freezed 实现不可变模型
- ❌ 不使用 AsyncValue - 使用
when()处理加载/错误状态 - ❌ 使用 Riverpod 的 setState - 使用提供者共享状态
- ❌ 将 ref 传递给函数 - 保持 ref 使用在组件/提供者内部
- ❌ 深层嵌套的 Consumer - 使用 ConsumerWidget 替代
- ❌ 不使用 family 参数 - 使用
.family为参数化提供者 - ❌ 全局 GoRouter 实例 - 使用 Provider 为路由器提供重定向逻辑
- ❌ 跨异步的 BuildContext - 在 await 之前存储值,而不是上下文
- ❌ 忽略 dispose - 在 ConsumerStatefulWidget 中清理控制器