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,若只需少量字形,应做子集化处理,否则会显著增大安装包。

资源路径大小写敏感:在 Windows 上开发、在 macOS 或 Linux 上构建时,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 构建变体与环境配置关系图 pubspec.yaml name / version dependencies assets / fonts environment.sdk 单一事实来源 Environment 配置对象 name apiBaseUrl enableLog 业务代码只依赖抽象 构建变体 Android flavor iOS Scheme --dart-define 原生侧执行 main_dev.dart runApp(App(devEnv)) main_prod.dart runApp(App(prodEnv)) main_staging.dart runApp(App(stgEnv)) 构建命令与产物 flutter build apk --flavor prod --dart-define=ENV=prod flutter run --flavor dev -t lib/main_dev.dart flavor 名称必须与原生侧声明一致

六、验证与常见问题

配置完成后,建议按以下顺序验证:先执行 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 声明。项目结构、依赖、资源与构建配置因此保持可预测,团队协作和持续集成才有稳定的基础。