3. Flutter项目结构与构建系统
一个 Flutter 工程能否长期维护,往往不取决于页面写得多快,而取决于项目骨架是否清晰、依赖与资源是否可追溯、不同环境能否用同一套代码安全地产出不同包。本节围绕 pubspec.yaml、目录组织、资源管理、多环境配置与构建变体五个方面,说明“应该怎么放、怎么写、怎么构建、怎么验证”。
一、pubspec.yaml:工程的单一事实来源
pubspec.yaml 是 Flutter 工程的清单文件,它同时承担四类职责:声明包元信息、声明依赖、声明资源、声明构建期配置。一个结构完整的示例如下:
name: shop_app
description: 跨平台商城示例
publish_to: 'none'
version: 1.2.0+12
environment:
sdk: '>=3.3.0 <4.0.0'
flutter: '>=3.19.0'
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0
provider: ^6.1.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.8
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/icons/
fonts:
- family: NotoSansSC
fonts:
- asset: assets/fonts/NotoSansSC-Regular.ttf
几个容易出错的点:version 的 +12 是构建号,Android 的 versionCode 与 iOS 的 CFBundleVersion 都由它派生;environment 中的 SDK 约束决定 flutter pub get 能否解析依赖;assets 若写成目录,必须以 / 结尾,否则会被当作单个文件解析而报错。依赖版本建议使用 ^ 允许兼容升级,但涉及原生插件的包在升级后应重新验证平台侧行为。
二、目录组织:按职责分层,而非按文件类型堆叠
Flutter 官方模板只给出 lib/main.dart,真实项目需要自行建立约定。推荐以“功能优先、层次其次”的方式组织:
lib/
├── main_dev.dart # 开发环境入口
├── main_prod.dart # 生产环境入口
├── app/
│ ├── app.dart # MaterialApp 与全局配置
│ └── router.dart
├── core/
│ ├── network/dio_client.dart
│ ├── storage/local_store.dart
│ └── env/environment.dart
├── features/
│ ├── home/
│ │ ├── data/home_repository.dart
│ │ ├── domain/home_model.dart
│ │ └── ui/home_page.dart
│ └── cart/
└── shared/widgets/
这样划分的好处是:features 内部自洽,删除或迁移一个业务模块时不会牵动全局;core 只放与业务无关的基础设施;shared 放可复用但无业务语义的组件。判断标准很简单——如果一个文件被三个以上 feature 引用且不含业务规则,它就应该下沉到 core 或 shared。
三、资源管理:声明、引用与体积控制
资源必须先在 pubspec.yaml 中声明,才能在代码中通过 AssetBundle 访问。声明目录后,代码里仍要写完整相对路径:
Image.asset('assets/images/banner_home.png')
资源管理的三个实践要点:其一,图片按用途分目录(images、icons、lottie),避免一个目录上千文件;其二,同一张图提供 2.0x、3.0x 变体时,Flutter 会按设备像素比自动选择,命名需遵循 assets/images/2.0x/banner_home.png 的约定;其三,字体文件体积大,中文字体动辄数 MB,若只需少量字形,应做子集化处理,否则会显著增大安装包。
Banner.png 与 banner.png 会被视为不同文件,务必统一为小写下划线命名。四、多环境配置:把差异收敛到一处
多环境的核心不是“写多个 main”,而是把环境差异集中到可注入的配置对象中。常见做法是定义一个 Environment 类,由入口文件传入具体值:
class Environment {
final String name;
final String apiBaseUrl;
final bool enableLog;
const Environment({required this.name, required this.apiBaseUrl, required this.enableLog});
}
const devEnv = Environment(
name: 'dev',
apiBaseUrl: 'https://dev-api.example.com',
enableLog: true,
);
const prodEnv = Environment(
name: 'prod',
apiBaseUrl: 'https://api.example.com',
enableLog: false,
);
入口文件 main_dev.dart 与 main_prod.dart 分别调用 runApp(App(env: devEnv)) 与 runApp(App(env: prodEnv))。这样业务代码只依赖 Environment 抽象,切换环境时无需改动任何页面逻辑。若环境数量较多,可用 --dart-define 在构建命令中注入,避免为每个环境维护独立入口文件。
五、构建变体:Flutter 侧与原生侧的配合
Flutter 的构建变体最终由原生构建系统执行:Android 使用 productFlavors,iOS 使用 Scheme 与 Configuration。Flutter 侧通过 --flavor 参数与之对应。以 Android 为例,在 android/app/build.gradle 中声明:
android {
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationIdSuffix ".dev"
resValue "string", "app_name", "商城Dev"
}
prod {
dimension "env"
resValue "string", "app_name", "商城"
}
}
}
构建命令相应变为:
flutter build apk --flavor prod --dart-define=ENV=prod
flutter run --flavor dev -t lib/main_dev.dart
需要注意:--flavor 必须与原生侧声明的 flavor 名称完全一致,否则会报找不到变体;iOS 侧还需在 Xcode 中为每个 Scheme 配置对应的 Bundle Identifier 与签名,否则归档时会失败。
六、验证与常见问题
配置完成后,建议按以下顺序验证:先执行 flutter pub get 确认依赖解析通过;再执行 flutter analyze 检查资源引用与未使用导入;随后用 flutter run --flavor dev 在真机确认接口地址与日志开关生效;最后用 flutter build apk --flavor prod 产出包,检查 applicationIdSuffix 是否使 dev 与 prod 可以共存安装。
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 资源加载失败 | 未在 pubspec 声明或路径大小写不一致 | 补声明并统一小写命名 |
| 找不到 flavor | 命令行名称与原生声明不一致 | 核对 build.gradle 与 Scheme |
| dev 与 prod 互相覆盖安装 | 未设置 applicationIdSuffix | 为 dev 添加后缀 |
| iOS 归档失败 | Scheme 未绑定签名与 Bundle ID | 在 Xcode 中逐 Scheme 配置 |
把上述五件事固定为工程约定后,新增一个环境只需增加一个 Environment 常量与一个入口文件,新增一个构建变体只需在原生侧补一段 flavor 声明。项目结构、依赖、资源与构建配置因此保持可预测,团队协作和持续集成才有稳定的基础。