mcpField
F
flutter-best-practices

flutter-best-practices

github.com/mauriciog87开发361 次安装38 周前更新
代码仓库

关于 flutter-best-practices

面向 Flutter 应用开发与代码审查的实践指南,适用于移动端、Web 和桌面端项目。内容涵盖架构、状态管理、性能、测试与发布。

主要功能

  1. 01按功能组织的分层架构
  2. 02Riverpod 与 BLoC 状态管理建议
  3. 03go_router 导航与深度链接
  4. 04性能优化与生命周期实践
  5. 05单元、组件及集成测试
  6. 06跨平台与发布指南

适用场景

  1. 01规划新的 Flutter 项目结构
  2. 02选择状态管理与导航方案
  3. 03审查性能和资源释放
  4. 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.md
platform-channels.md
state-management.md
testing.md

详细介绍

Flutter 最佳实践

面向移动端、Web 和桌面平台构建可用于生产环境的 Flutter 应用的专家指南。

快速入门

开始新的 Flutter 项目或审查代码时:

  1. 架构:采用以功能为先的文件夹结构,清晰划分职责
  2. 状态管理:新项目优先使用 Riverpod;复杂的异步流程使用 BLoC
  3. 导航:使用 go_router 支持深度链接和 Web
  4. 测试:遵循测试金字塔——单元测试 → Widget 测试 → 集成测试
  5. 性能:使用 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
具有本地状态的交互式 UIStatefulWidget
在 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 发生变化时重建 Widget
  • ref.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

参考资料

有关特定主题的详细指南:

评论

支持 Markdown