关于 flutter-best-practices
面向 Flutter 应用开发与代码审查的实践指南,适用于移动端、Web 和桌面端项目。内容涵盖架构、状态管理、性能、测试与发布。
主要功能
- 01按功能组织的分层架构
- 02Riverpod 与 BLoC 状态管理建议
- 03go_router 导航与深度链接
- 04性能优化与生命周期实践
- 05单元、组件及集成测试
- 06跨平台与发布指南
适用场景
- 01规划新的 Flutter 项目结构
- 02选择状态管理与导航方案
- 03审查性能和资源释放
- 04制定 Flutter 应用测试计划
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mauriciog87/ flutter-agent-skill --skill flutter-best-practices安装后在 Claude Code 中输入/flutter-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/performance.mdplatform-channels.mdstate-management.mdtesting.md详细介绍
Flutter 最佳实践
面向移动端、Web 和桌面平台构建可用于生产环境的 Flutter 应用的专家指南。
快速入门
开始新的 Flutter 项目或审查代码时:
- 架构:采用以功能为先的文件夹结构,清晰划分职责
- 状态管理:新项目优先使用 Riverpod;复杂的异步流程使用 BLoC
- 导航:使用 go_router 支持深度链接和 Web
- 测试:遵循测试金字塔——单元测试 → Widget 测试 → 集成测试
- 性能:使用
const构造函数、ListView.builder和适当的资源释放模式
架构模式
三层架构
将代码划分为不同的层,以便于维护:
lib/
├── features/
│ └── feature_name/
│ ├── presentation/ # UI widgets, screens
│ ├── domain/ # Business logic, use cases, entities
│ └── data/ # Repositories, data sources, models
├── core/
│ ├── theme/ # AppTheme, colors, typography
│ ├── router/ # GoRouter configuration
│ └── utils/ # Shared utilities
└── main.dart
原则:
- 依赖规则:高层只能依赖低层
- 功能 API 接口:通过接口隐藏实现细节
- 仓库模式:抽象数据访问,以便进行测试
Widget 架构
Widget 类型决策树:
| 场景 | 使用 |
|---|---|
| 静态 UI,没有变化的状态 | StatelessWidget |
| 具有本地状态的交互式 UI | StatefulWidget |
| 在 Widget 树中向下共享的数据 | InheritedWidget / Riverpod |
| 复杂且可复用的 UI 组件 | 自定义 Widget 类 |
StatefulWidget 生命周期(关键):
class _MyWidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
// One-time initialization (controllers, subscriptions)
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Called when InheritedWidget dependencies change
}
@override
Widget build(BuildContext context) {
// Keep fast and synchronous - no heavy computation
return Container();
}
@override
void dispose() {
// CRITICAL: Dispose controllers, cancel subscriptions
_controller.dispose();
_subscription?.cancel();
super.dispose();
}
}
黄金法则:始终释放控制器、滚动控制器和文本控制器,并取消流订阅。
状态管理
Riverpod(新项目推荐)
设置:
// main.dart
void main() {
runApp(ProviderScope(child: MyApp()));
}
// providers.dart
final counterProvider = StateProvider<int>((ref) => 0);
final userProvider = FutureProvider<User>((ref) async {
return await ref.watch(authRepositoryProvider).getCurrentUser();
});
@riverpod
class TodoList extends _$TodoList {
@override
List<Todo> build() => [];
void add(Todo todo) => state = [...state, todo];
void toggle(String id) {
state = state.map((t) =>
t.id == id ? t.copyWith(completed: !t.completed) : t
).toList();
}
}
使用模式:
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch for UI updates
final count = ref.watch(counterProvider);
final asyncUser = ref.watch(userProvider);
final todos = ref.watch(todoListProvider);
// Read for one-time access (callbacks)
void onPressed() {
ref.read(todoListProvider.notifier).add(newTodo);
}
return asyncUser.when(
data: (user) => Text(user.name),
loading: () => CircularProgressIndicator(),
error: (err, stack) => ErrorWidget(err),
);
}
}
主要方法:
ref.watch(provider):当 provider 发生变化时重建 Widgetref.read(provider):获取一次值(在回调中使用,不要在 build 中使用)ref.listen(provider):监听变化并执行副作用
BLoC 模式(适用于复杂的异步流程)
适用场景:
- 复杂的事件驱动架构
- 需要严格分离业务逻辑的应用
- 熟悉响应式编程的团队
// Events
abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
// States
class CounterState {
final int count;
CounterState(this.count);
}
// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<CounterIncrementPressed>((event, emit) {
emit(CounterState(state.count + 1));
});
}
}
// Usage
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('Count: ${state.count}');
},
)
状态选择指南
| 使用场景 | 解决方案 |
|---|---|
| 简单的本地 Widget 状态 | StatefulWidget + setState() |
| 在多个 Widget 间共享的单个值 | StateProvider |
| 异步数据(API 调用) | FutureProvider |
| 基于流的数据 | StreamProvider |
| 包含业务逻辑的复杂状态 | StateNotifier / BLoC |
| 全局应用状态 | 使用带作用域 provider 的 Riverpod |
导航
go_router(推荐)
设置:
final _router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
// Auth guard
final isLoggedIn = authState.isAuthenticated;
final isLoggingIn = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn && isLoggingIn) return '/';
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) => DetailScreen(
id: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: '/login',
builder: (context, state) => LoginScreen(),
),
],
);
// In MaterialApp
MaterialApp.router(routerConfig: _router);
导航:
// Navigate
context.go('/details/123');
context.push('/details/123'); // Preserves navigation stack
context.pop();
// With query parameters
context.goNamed('details', queryParameters: {'tab': 'reviews'});
深度链接设置:
Android(AndroidManifest.xml):
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="yourdomain.com"/>
</intent-filter>
iOS(info.plist):
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>yourscheme</string>
</array>
</dict>
</array>
Widget 模式
布局要点
// Common layout patterns
Column( // Vertical layout
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [...],
)
Row( // Horizontal layout
children: [
Expanded(flex: 2, child: ...), // Takes 2/3 space
Expanded(flex: 1, child: ...), // Takes 1/3 space
],
)
Stack( // Overlapping widgets
children: [
Positioned.fill(child: Background()),
Align(alignment: Alignment.bottomCenter, child: ...),
],
)
// Responsive layouts
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return DesktopLayout();
}
return MobileLayout();
},
)
列表优化
较大或无限列表始终使用 builder:
// Good - lazy loading, only builds visible items
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index].title),
),
)
// For grids with images
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
),
itemCount: items.length,
itemBuilder: (context, index) => ImageCard(items[index]),
)
// With custom scroll effects
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(title: Text('Title')),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(...),
childCount: items.length,
),
),
],
)
表单处理
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
validator: (value) {
if (value?.isEmpty ?? true) return 'Required';
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// Process form
}
},
child: Text('Submit'),
),
],
),
);
}
}
性能优化
关键性能规则
| 规则 | 实现方式 | 优先级 |
|---|---|---|
使用 const 构造函数 | const Text('Hello') | 关键 |
| 使用 builder 构建列表 | ListView.builder | 关键 |
| 释放控制器 | controller.dispose() | 关键 |
| 缓存开销大的操作 | 对繁重任务使用 compute() | 高 |
使用 RepaintBoundary | 包裹动画 Widget | 中 |
| 避免裁剪 | 尽量减少 Clip Widget | 中 |
内存管理
// Always check mounted before setState in async
void _loadData() async {
final data = await fetchData();
if (mounted) { // CRITICAL
setState(() => _data = data);
}
}
// Limit image cache for memory-constrained apps
PaintingBinding.instance.imageCache.maximumSize = 100;
PaintingBinding.instance.imageCache.maximumSizeBytes = 50 * 1024 * 1024;
// Use weak references for long-lived callbacks
final weakRef = WeakReference(object);
使用 Isolate
将繁重的计算任务从 UI 线程卸载:
// Heavy computation
final result = await compute(parseJson, largeJsonString);
// For complex parsing
Future<List<Model>> parseModels(String jsonString) async {
return compute((str) {
final decoded = json.decode(str) as List;
return decoded.map((e) => Model.fromJson(e)).toList();
}, jsonString);
}
DevTools 性能分析
启用以下标志以进行调试:
// In main.dart for debugging
void main() {
// Visualize widget rebuilds
debugRepaintRainbowEnabled = true;
// Show paint bounds
debugPaintSizeEnabled = true;
runApp(MyApp());
}
DevTools 主要视图:
- Widget 重建次数:识别过多的重建
- 性能:帧耗时、着色器编译
- 内存:堆快照、分配跟踪
- 网络:监控 API 调用
测试
测试金字塔
▲ Integration (Full flows)
╱ ╲
╱ ╲ Widget (UI components)
╱ ╲
╱ ╲
╱_________╲
Unit (Logic, pure Dart)
单元测试
test('can calculate total price', () {
// Arrange
final cart = Cart(items: [
Item(price: 10, quantity: 2),
Item(price: 5, quantity: 1),
]);
// Act
final total = cart.total;
// Assert
expect(total, equals(25));
});
// With Riverpod
test('counter increments', () {
final container = ProviderContainer();
addTearDown(container.dispose);
final notifier = container.read(counterProvider.notifier);
notifier.state = 5;
notifier.state++;
expect(container.read(counterProvider), equals(6));
});
Widget 测试
testWidgets('displays user name', (WidgetTester tester) async {
// Arrange
await tester.pumpWidget(
MaterialApp(
home: UserProfile(user: User(name: 'John')),
),
);
// Act & Assert
expect(find.text('John'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsNothing);
});
testWidgets('tapping button increments counter', (tester) async {
await tester.pumpWidget(MyApp());
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
Golden 测试(视觉回归)
testGoldens('UserProfile renders correctly', (tester) async {
final builder = GoldenBuilder.grid(columns: 2)
..addScenario('Light', UserProfile(user: mockUser))
..addScenario('Dark', Theme(data: darkTheme, child: UserProfile(user: mockUser)));
await tester.pumpWidgetBuilder(builder.build());
await screenMatchesGolden(tester, 'user_profile');
});
// Update goldens: flutter test --update-goldens --tags=golden
集成测试
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('full login flow', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, '[email protected]');
await tester.enterText(find.byType(TextField).last, 'password');
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text('Welcome'), findsOneWidget);
});
}
网络
HTTP 服务模式
class ApiService {
final Dio _dio;
ApiService({Dio? dio}) : _dio = dio ?? Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
)) {
_dio.interceptors.add(AuthInterceptor());
_dio.interceptors.add(LogInterceptor());
}
Future<List<User>> getUsers() async {
final response = await _dio.get('/users');
return (response.data as List)
.map((json) => User.fromJson(json))
.toList();
}
}
// With Riverpod
final apiServiceProvider = Provider<ApiService>((ref) {
return ApiService();
});
final usersProvider = FutureProvider<List<User>>((ref) async {
final api = ref.watch(apiServiceProvider);
return api.getUsers();
});
JSON 序列化
// With freezed (recommended)
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
@Default([]) List<String> tags,
DateTime? createdAt,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) =>
_$UserFromJson(json);
}
// Manual approach
class User {
final String id;
final String name;
User({required this.id, required this.name});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
name: json['name'] as String,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
};
}
Firebase 集成
Firebase 设置
# Install FlutterFire CLI
dart pub global activate flutterfire_cli
# Configure project
flutterfire configure --project=your-project-id
云消息(推送通知)
class NotificationService {
final FirebaseMessaging _messaging;
NotificationService(this._messaging);
Future<void> initialize() async {
// Request permissions
final settings = await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
// Get FCM token
final token = await _messaging.getToken();
await _saveToken(token);
// Listen to token refresh
_messaging.onTokenRefresh.listen(_saveToken);
// Handle foreground messages
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
// Handle background/terminated
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
_handleNotificationTap(initialMessage);
}
}
@pragma('vm:entry-point')
static Future<void> _firebaseMessagingBackgroundHandler(
RemoteMessage message,
) async {
await Firebase.initializeApp();
// Handle background message
}
}
// Register in main
FirebaseMessaging.onBackgroundMessage(
NotificationService._firebaseMessagingBackgroundHandler,
);
跨平台注意事项
平台检测
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
bool get isWeb => kIsWeb;
bool get isMobile => !kIsWeb && (Platform.isIOS || Platform.isAndroid);
bool get isDesktop => !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
// Conditional UI
if (Platform.isIOS) {
return CupertinoButton(child: Text('Done'), onPressed: () {});
}
return ElevatedButton(child: Text('Done'), onPressed: () {});
自适应布局
// Using flutter_adaptive_scaffold
AdaptiveLayout(
primaryNavigation: SlotLayout(config: {
Breakpoints.mediumAndUp: SlotLayout.from(
builder: (_) => NavigationRail(destinations: [...]),
),
}),
bottomNavigation: SlotLayout(config: {
Breakpoints.small: SlotLayout.from(
builder: (_) => BottomNavigationBar(items: [...]),
),
}),
body: SlotLayout(config: {
Breakpoints.small: SlotLayout.from(builder: (_) => MobileBody()),
Breakpoints.mediumAndUp: SlotLayout.from(builder: (_) => DesktopBody()),
}),
)
平台通道
Flutter 端:
class PlatformChannelService {
static const MethodChannel _channel =
MethodChannel('com.example.app/channel');
Future<String?> getNativeData() async {
try {
final result = await _channel.invokeMethod<String>('getNativeData');
return result;
} on PlatformException catch (e) {
print('Error: ${e.message}');
return null;
}
}
}
动画
隐式动画
// Simple animations that trigger on value change
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 200 : 100,
height: isExpanded ? 200 : 100,
color: isExpanded ? Colors.red : Colors.blue,
child: child,
)
AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: isVisible ? 1.0 : 0.0,
child: child,
)
显式动画
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: ScaleTransition(
scale: _animation,
child: child,
),
);
}
}
Hero 动画
// Source page
Hero(
tag: 'image-${item.id}',
child: Image.network(item.imageUrl),
)
// Destination page (same tag)
Hero(
tag: 'image-${item.id}',
child: Image.network(item.imageUrl),
)
生产部署
构建命令
# Android App Bundle (recommended for Play Store)
flutter build appbundle --release --obfuscate --split-debug-info=symbols/
# Android APK
flutter build apk --release --split-per-abi
# iOS
flutter build ios --release
# Web
flutter build web --release
# Desktop
flutter build windows --release
flutter build macos --release
flutter build linux --release
代码混淆
生产环境中务必进行混淆:
flutter build appbundle \
--obfuscate \
--split-debug-info=symbols/
保存符号文件,以便对崩溃信息进行符号化。
CI/CD 流水线(GitHub Actions 示例)
name: Flutter CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
- run: flutter pub get
- run: flutter analyze
- run: flutter test
- run: flutter build apk --release
参考资料
有关特定主题的详细指南:
- 状态管理深入指南:参见 references/state-management.md
- 测试模式:参见 references/testing.md
- 性能优化:参见 references/performance.md
- 平台集成:参见 references/platform-channels.md
评论
支持 Markdown