目录结构
好的目录结构是可维护性的基础。我在多个项目中验证过的结构(以 Flutter 为例):
lib/
├── core/ # 核心基础设施
│ ├── api/ # HTTP 客户端、拦截器
│ ├── storage/ # 本地存储抽象
│ ├── router/ # 路由配置
│ └── di/ # 依赖注入
│
├── features/ # 按功能划分(Feature-First)
│ ├── auth/
│ │ ├── data/ # 数据层:API、本地DB
│ │ ├── domain/ # 领域层:实体、用例
│ │ └── presentation/ # 表现层:页面、Widget、Provider
│ ├── home/
│ └── profile/
│
├── shared/ # 跨功能共用
│ ├── widgets/ # 通用 UI 组件
│ ├── utils/ # 工具函数
│ └── constants/ # 常量定义
│
└── main.dart
关键原则:按功能(Feature)划分,而非按类型(Type)划分。按类型划分(pages/、models/、services/)在项目变大后会造成大量跨目录的依赖,修改一个功能要改四五个文件夹。
分层架构(Clean Architecture 简化版)
对于中大型项目,我推荐三层架构:
┌─────────────────────────────┐
│ Presentation Layer │ UI、ViewModel/Provider/BLoC
├─────────────────────────────┤
│ Domain Layer │ 业务逻辑、实体、Repository 接口
├─────────────────────────────┤
│ Data Layer │ API、数据库、Repository 实现
└─────────────────────────────┘
- Domain 层不依赖任何框架,只有纯 Dart/TypeScript 代码。这让它可以被独立测试,也方便未来迁移框架。
- Repository 模式:Domain 层定义接口,Data 层实现。这样切换 API 地址、更换数据库引擎不会影响业务逻辑。
// domain/repositories/user_repository.dart(接口)
abstract class UserRepository {
Future<User> getUserById(String id);
Future<void> updateProfile(User user);
}
// data/repositories/user_repository_impl.dart(实现)
class UserRepositoryImpl implements UserRepository {
final ApiClient _api;
final LocalDatabase _db;
@override
Future<User> getUserById(String id) async {
// 先查缓存,没有再请求 API
final cached = await _db.getUser(id);
if (cached != null) return cached;
final user = await _api.fetchUser(id);
await _db.saveUser(user);
return user;
}
}
平台差异隔离
有些 UI 或行为在 Android 和 iOS 上应该不同(如 iOS 的 Cupertino 风格组件)。隔离平台差异而非到处写 if Platform.isIOS:
// shared/widgets/platform_dialog.dart
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
Future<bool?> showPlatformDialog({
required BuildContext context,
required String title,
required String message,
}) {
if (Platform.isIOS) {
return showCupertinoDialog<bool>(
context: context,
builder: (_) => CupertinoAlertDialog(
title: Text(title),
content: Text(message),
actions: [
CupertinoDialogAction(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
CupertinoDialogAction(isDefaultAction: true, child: const Text('确定'), onPressed: () => Navigator.pop(context, true)),
],
),
);
}
return showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('确定')),
],
),
);
}
共享业务逻辑
跨平台框架最大的价值就是共享逻辑。以下这些内容应该尽量平台无关:
- 数据模型(User、Product、Order)
- API 请求与响应处理
- 本地存储的 key 定义
- 日期格式化、货币转换等工具函数
- 表单验证规则
- 业务状态机(如订单状态流转)
而以下内容应该允许平台差异:
- 导航手势(Android 返回键 vs iOS 滑动返回)
- 分享功能(平台原生分享面板)
- 通知权限申请时机和文案
- 支付(Google Pay vs Apple Pay)
测试策略
跨平台项目的测试金字塔:
/\
/ \
/ E2E\ 集成测试:patrol / detox
/______\
/ Widget \ Widget 测试:flutter_test
/____________\
/ Unit Tests \ 单元测试:Domain 层逻辑,100% 覆盖率
/________________\
Domain 层的用例(Use Case)是最值得写测试的地方,它们是纯函数,没有 Flutter/React Native 依赖,测试简单快速:
// test/domain/get_user_test.dart
void main() {
late GetUserUseCase useCase;
late MockUserRepository mockRepo;
setUp(() {
mockRepo = MockUserRepository();
useCase = GetUserUseCase(mockRepo);
});
test('应该返回用户数据', () async {
when(mockRepo.getUserById('123'))
.thenAnswer((_) async => User(id: '123', name: 'Alice'));
final result = await useCase.execute('123');
expect(result.name, 'Alice');
});
}
CI/CD 自动化
GitHub Actions 是独立开发者的最佳选择,免费额度足够用。一个基础的 Flutter CI 工作流:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
- run: flutter pub get
- run: flutter analyze
- run: flutter test --coverage
build-android:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- run: flutter build appbundle --release
核心原则
「不要为了跨平台而跨平台,为了用户体验而正确处理平台差异。」
- 功能优先于完美架构:先让 App 能用,再重构
- 延迟抽象:当你第三次写重复代码时再抽象,不要过早
- 平台一致 ≠ 平台相同:用户期望 iOS App 有 iOS 的感觉,Android App 有 Android 的感觉
- 真机测试不可省:模拟器无法完全模拟性能、传感器和系统交互
- 持续集成是底线:每次提交都跑测试,能省下大量调试时间