项目结构设计:原生工程与Flutter模块的目录组织方式

聊到项目结构,我得先说说自己的血泪史。几年前我第一次把Flutter模块塞进一个老安卓项目时,目录乱得像一锅粥——Dart代码和Java文件混在一起,Gradle配置互相打架,连.gitignore都不知道该写什么。后来我花了整整两天重构结构,才明白一个道理:项目结构不是摆设,它是团队协作的骨架

今天咱们就掰开揉碎,聊聊原生工程和Flutter模块到底该怎么组织。说白了就两个方向:单工程方案和多工程方案。各有各的适用场景,选错了后面有你受的。

一、单工程方案:Flutter模块嵌入原生项目

单工程方案,就是把Flutter模块当作一个普通模块,直接放在安卓项目的根目录下。我早期做混合开发时,这是最常用的方式。

典型的目录结构长这样:

MyApp/
├── android/                    # 原生安卓工程
│   ├── app/
│   │   ├── src/
│   │   └── build.gradle
│   ├── settings.gradle
│   └── build.gradle
├── flutter_module/             # Flutter模块
│   ├── lib/
│   │   ├── main.dart
│   │   └── pages/
│   ├── pubspec.yaml
│   └── build.gradle
├── ios/                        # iOS工程(略)
└── README.md

这种结构的好处很明显:简单直接。你不需要操心多仓库的版本管理,代码都在一个项目里,IDE打开就能跑。我在一个中小型项目里用过这种方案,团队只有5个人,开发效率确实高。

但坑也不少。我曾经遇到过一个场景:安卓团队和Flutter团队并行开发,结果Flutter模块的pubspec.yaml一改,安卓那边就得重新sync Gradle。更头疼的是,如果你们用Git做版本管理,这个仓库会越来越大——Flutter的编译产物动不动就几百兆。

适用场景:

  • 团队规模小(10人以内)
  • 项目初期,快速验证可行性
  • Flutter模块改动频率低

注意:单工程方案下,Flutter模块的build.gradle需要和安卓主工程的Gradle版本保持一致。我见过有人因为版本不一致,编译时直接报"Could not resolve all artifacts",排查了半天才发现是Gradle版本冲突。

二、多工程方案:原生与Flutter独立仓库

多工程方案,说白了就是把Flutter模块抽出来,作为一个独立的Git仓库。原生工程通过依赖方式引入Flutter模块。这种方式在大型项目中更常见。

目录结构变成这样:

# 原生安卓仓库
NativeApp/
├── app/
├── settings.gradle
└── build.gradle

# Flutter模块仓库
FlutterModule/
├── lib/
├── pubspec.yaml
├── build.gradle
└── .gitignore

原生工程通过settings.gradle引用Flutter模块:

// settings.gradle
include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
    settingsDir.parentFile,
    'FlutterModule/.android/include_flutter.groovy'
))

我个人习惯用多工程方案,尤其是团队超过15人时。为什么?解耦。安卓团队和Flutter团队可以各自独立开发、独立发布,互不干扰。我曾经在一个项目里,安卓团队用Kotlin写业务逻辑,Flutter团队用Dart写UI组件,两个仓库各自有CI/CD流水线,互不阻塞。

但多工程方案也有代价:集成成本高。每次Flutter模块发布新版本,安卓团队都得手动更新依赖版本。如果你们没有完善的版本管理机制,很容易出现"安卓那边还在用旧版Flutter,但Flutter团队已经改了接口"的尴尬局面。

我的建议:如果选多工程方案,一定要用语义化版本控制(SemVer)。Flutter模块的每个版本都要有清晰的changelog,安卓团队升级时能明确知道改了啥。我曾经吃过亏——Flutter团队偷偷改了一个接口参数类型,安卓那边没注意,上线后直接崩溃。

三、两种方案的对比与选择

我整理了一张对比表,方便你快速决策:

维度 单工程方案 多工程方案
团队规模 小团队(≤10人) 中大型团队(>10人)
开发效率 高,无需跨仓库 中,需要版本管理
编译速度 慢,全量编译 快,增量编译
版本管理 简单,单仓库 复杂,多仓库
CI/CD 简单,单流水线 复杂,多流水线
适用阶段 项目初期/原型验证 项目成熟期/持续迭代

你想想看,如果你的项目还在MVP阶段,团队就两三个人,用多工程方案纯粹是给自己找麻烦。反过来,如果项目已经上线,团队分工明确,单工程方案会变成瓶颈——每次合并代码都像在打仗。

四、混合方案:折中之道

有没有两全其美的办法?有。我最近在做一个项目时,用了混合方案

  • 开发阶段用单工程方案,方便快速调试
  • 发布阶段用多工程方案,保证版本隔离

具体做法是:在原生工程里通过Git Submodule或Android Studio的Module依赖,把Flutter模块作为子模块引入。开发时直接修改子模块代码,发布时子模块独立打Tag。

嗯,这里要注意:Submodule虽然好用,但团队成员如果不熟悉Git操作,容易搞乱。我建议在团队内部写一份Submodule操作指南,把常见的pull、push、merge步骤写清楚。

五、避坑指南

最后分享几个我踩过的坑:

  • 不要混用Gradle版本:Flutter模块的Gradle版本必须和安卓主工程一致,否则编译报错。我曾经因为这个问题折腾了一下午。
  • 注意.gitignore:Flutter的build目录、.dart_tool目录、.packages文件都不要提交到Git仓库。我见过有人把整个Flutter编译产物提交了,仓库直接膨胀到2GB。
  • 统一代码风格:不管用哪种方案,建议在项目根目录放一个.editorconfig文件,统一缩进、换行符等。否则安卓团队用4空格,Flutter团队用2空格,代码review时全是格式问题。

核心原则:项目结构是为团队协作服务的,不是为技术炫技的。选方案时,先问自己三个问题:团队多少人?项目到什么阶段?Flutter模块改动频率高不高?答案清楚了,方案自然就出来了。

项目结构方案决策流程图 开始选择方案 团队规模 > 10人? 多工程方案 独立仓库,版本管理 单工程方案 简单直接,快速迭代 Flutter模块改动频繁? 混合方案 开发单工程,发布多工程 保持原方案 当前方案已满足需求 核心原则:结构服务于团队,不服务于技术

好了,项目结构这块就聊到这儿。记住,没有银弹,只有最适合你团队的方案。下一章咱们聊聊Flutter模块的构建配置,到时候我会分享一些Gradle脚本的骚操作。


公众号:蓝海资料掘金营,微信deep3321