Flutter 架构模式与最佳实践

Flutter 大型项目架构:Clean Architecture、DDD、模块化、依赖注入、大型项目组织与团队协作最佳实践。

开篇:架构是项目的生命线

小型 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 分层职责

职责依赖方向
PresentationUI、状态管理、用户交互→ Domain
Domain实体、业务规则、UseCase 接口←(不依赖其他层)
DataRepository 实现、数据源(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/ — 插件开发与模块化

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Flutter」更多文章

  1. Widget 体系与布局系统
  2. Flutter 状态管理全解析
  3. Flutter 测试策略与自动化