开篇:架构是项目的生命线
小型 Flutter 项目可以靠直觉和经验组织代码,但当团队扩大到 10 人以上、功能模块超过 50 个、代码行数超过 10 万行时,没有清晰的架构指导,代码会迅速退化为"大泥球"(Big Ball of Mud)——依赖关系混乱、修改一处引发三处连锁反应、新人上手周期以月计。
架构的核心目的是隔离变化。UI 会频繁调整,但业务规则相对稳定;数据存储可能从 SQLite 迁移到服务器,但业务逻辑不应受影响。Clean Architecture 通过同心圆分层实现了这种隔离,而 DDD(领域驱动设计)为复杂业务域提供了建模方法论。
本章将结合 Flutter 的实际特点,介绍从项目结构到具体实现的完整架构方案。
一、Clean Architecture 分层
lib/
├── main.dart # 入口
├── app.dart # MaterialApp 配置
├── core/ # 核心层(跨层复用)
│ ├── constants/
│ ├── errors/
│ ├── extensions/
│ ├── theme/
│ └── utils/
├── features/ # 功能模块(按业务划分)
│ └── auth/ # 示例:认证模块
│ ├── data/ # 数据层(Repository 实现)
│ ├── domain/ # 领域层(Entities, UseCases)
│ └── presentation/ # 表现层(Pages, Widgets, Providers)
├── shared/ # 共享组件
│ ├── widgets/ # 通用 UI 组件
│ └── services/ # 共享服务
└── injection.dart # 依赖注入配置
1.1 分层职责
| 层 | 职责 | 依赖方向 |
|---|---|---|
| Presentation | UI、状态管理、用户交互 | → Domain |
| Domain | 实体、业务规则、UseCase 接口 | ←(不依赖其他层) |
| Data | Repository 实现、数据源(API/DB) | → Domain |
// ===== Domain 层 =====
// domain/entities/user.dart
class User {
final String id;
final String email;
final String displayName;
User({required this.id, required this.email, required this.displayName});
}
// domain/repositories/auth_repository.dart
abstract class AuthRepository {
Future<User?> getCurrentUser();
Future<User> signIn(String email, String password);
Future<void> signOut();
}
// domain/usecases/sign_in.dart
class SignInUseCase {
final AuthRepository repository;
SignInUseCase(this.repository);
Future<User> call(String email, String password) async {
if (email.isEmpty || password.isEmpty) {
throw ValidationException('邮箱和密码不能为空');
}
return await repository.signIn(email, password);
}
}
// ===== Data 层 =====
// data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remote;
final AuthLocalDataSource local;
AuthRepositoryImpl({required this.remote, required this.local});
@override
Future<User> signIn(String email, String password) async {
final user = await remote.signIn(email, password);
await local.cacheUser(user);
return user;
}
@override
Future<User?> getCurrentUser() async {
return await local.getCachedUser();
}
@override
Future<void> signOut() async {
await remote.signOut();
await local.clearCache();
}
}
// data/datasources/auth_remote_datasource.dart
abstract class AuthRemoteDataSource {
Future<User> signIn(String email, String password);
Future<void> signOut();
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final Dio dio;
AuthRemoteDataSourceImpl(this.dio);
@override
Future<User> signIn(String email, String password) async {
final response = await dio.post('/auth/signin', data: {
'email': email,
'password': password,
});
return User.fromJson(response.data);
}
@override
Future<void> signOut() async {
await dio.post('/auth/signout');
}
}
// ===== Presentation 层 =====
// presentation/providers/auth_provider.dart
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
late SignInUseCase _signIn;
late SignOutUseCase _signOut;
@override
FutureOr<User?> build() async {
_signIn = ref.read(signInUseCaseProvider);
_signOut = ref.read(signOutUseCaseProvider);
return await ref.read(authRepositoryProvider).getCurrentUser();
}
Future<void> signIn(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => _signIn(email, password));
}
Future<void> signOut() async {
await _signOut();
state = const AsyncData(null);
}
}
// presentation/pages/sign_in_page.dart
class SignInPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
return Scaffold(
body: authState.when(
data: (user) => user != null ? HomePage() : SignInForm(),
loading: () => Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('错误: $e')),
),
);
}
}
一句话总结:Clean Architecture 的核心是依赖方向向内指向 Domain——Domain 知不知道 UI 和数据存储都无所谓,这正是业务逻辑稳定性的根本保障。
二、模块化与 Monorepo
2.1 包拆分
workspace/
├── apps/
│ ├── mobile_app/ # 移动端应用入口
│ ├── web_app/ # Web 应用入口
│ └── admin_app/ # 管理后台入口
├── packages/
│ ├── core/ # 共享核心库
│ ├── design_system/ # UI 组件库
│ ├── auth_module/ # 认证模块
│ ├── payment_module/ # 支付模块
│ └── analytics_module/ # 分析模块
└── melos.yaml # monorepo 管理
# melos.yaml
name: my_company_workspace
packages:
- apps/**
- packages/**
scripts:
analyze:
run: melos exec -- flutter analyze
test:
run: melos exec -- flutter test
build:
run: melos exec -- flutter build
2.2 路由模块化
// auth_module/lib/routes.dart
class AuthRoutes {
static const signIn = '/auth/signin';
static const signUp = '/auth/signup';
static const forgotPassword = '/auth/forgot-password';
}
final authRouter = GoRoute(
path: '/auth',
routes: [
GoRoute(path: 'signin', builder: (_, __) => SignInPage()),
GoRoute(path: 'signup', builder: (_, __) => SignUpPage()),
],
);
// 在主应用中合并
final router = GoRouter(
routes: [
...authRouter.routes,
...paymentRouter.routes,
],
);
一句话总结:Monorepo 架构将代码拆分为可独立开发、测试、发布的模块包,既保持了代码复用性,又避免了单个仓库的复杂度失控。
三、依赖注入
// injection.dart
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void setupDependencies() {
// API Client
getIt.registerLazySingleton<Dio>(() => ApiClient().dio);
// Data Sources
getIt.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(getIt()),
);
getIt.registerLazySingleton<AuthLocalDataSource>(
() => AuthLocalDataSourceImpl(),
);
// Repositories
getIt.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remote: getIt(),
local: getIt(),
),
);
// Use Cases
getIt.registerLazySingleton(() => SignInUseCase(getIt()));
getIt.registerLazySingleton(() => SignOutUseCase(getIt()));
}
// main.dart
void main() {
setupDependencies();
runApp(MyApp());
}
一句话总结:依赖注入将对象的创建和使用解耦,让测试时可以轻松替换 Mock 实现,是 Clean Architecture 不可或缺的配套机制。
四、代码规范与团队协作
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
- always_declare_return_types
- avoid_print
- prefer_const_constructors
- prefer_final_locals
- sort_constructors_first
- avoid_unused_constructor_parameters
- lines_longer_than_80_chars: false
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
errors:
invalid_annotation_target: ignore
一句话总结:统一的代码规范(lints)和自动格式化(dart format)是团队协作的基础设施,应在 CI 中强制执行。
FAQ
Q1: 小型项目需要 Clean Architecture 吗?
不需要。Clean Architecture 增加了抽象层数,小型项目的收益抵不过复杂度。建议:
- < 5 个页面:直接组织即可
- 5-20 个页面:Repository 模式 + 简单分层
20 个页面:考虑 Clean Architecture
Q2: Feature-First 还是 Layer-First 组织代码?
Feature-First(按业务模块)适合大多数项目:
- 改认证功能时只需关注
features/auth - 新成员容易定位相关代码
Layer-First(按技术分层)仅在超大型共享库中适用。
Q3: 如何处理跨模块通信?
- 消息总线:全局事件(如用户登出通知所有模块清理数据)
- 共享 Repository:模块间通过 Domain 层接口通信
- 避免直接 Widget 引用:防止编译耦合
Q4: GetIt 和 Riverpod 可以一起用吗?
可以但不推荐。两种 DI 方案并存会增加困惑。建议:
- 使用 Riverpod 的项目:直接用
Provider做 DI - 不使用 Riverpod 的项目:GetIt 是轻量选择
Q5: 大型项目如何避免导入混乱?
使用 package: 导入(而非相对路径 ../),并启用 directives_ordering lint:
// ✅ 使用包导入
import 'package:my_app/features/auth/domain/entities/user.dart';
// ❌ 避免深层相对路径
import '../../../../domain/entities/user.dart';
相关阅读
- https://plumephp.com/flutter-state-management/ — 状态管理全解析
- https://plumephp.com/flutter-testing/ — 测试策略与自动化
- https://plumephp.com/flutter-package-development/ — 插件开发与模块化
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。