Flutter 基础体系 · 第 4/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Flutter 项目工具链:SDK、Pub、Flavor、代码生成和环境配置

Flutter 项目工具链解决的不是同一个问题:

  • SDK 决定 Dart 语言、Flutter 框架、编译器、命令行工具和平台构建能力。
  • Pub 解析 Dart/Flutter 包的依赖关系,并生成项目使用这些包所需的配置。
  • Flavor 为同一份应用代码提供多个产品变体,例如开发、测试、生产和不同品牌。
  • 代码生成 根据源码或 schema 自动产生 Dart 代码,减少重复实现,但会引入生成阶段和版本约束。
  • 环境配置 把 API 地址、功能开关、应用标识等部署差异传入应用,同时决定哪些值可以公开、哪些值不能放进客户端。

这些层次相互影响,但不能混为一谈。比如,Flavor 通常由 Android Gradle 或 Xcode 原生配置识别,--dart-define 则是 Flutter/Dart 编译期配置;两者可以一起使用,但不是同一个机制。


一、先建立工具链的运行模型

一个 Flutter 项目从源码到可运行应用,大致经过以下路径:

flowchart LR
    A[Flutter SDK] --> B[flutter 命令]
    B --> C[Pub 解析依赖]
    C --> D[.dart_tool/package_config.json]
    D --> E[Dart 分析与编译]
    E --> F[build_runner 代码生成]
    F --> E
    E --> G[Flutter 构建系统]
    G --> H[Android/iOS/Web/桌面产物]
    I[Flavor 原生配置] --> G
    J[--dart-define 环境配置] --> E

一次典型的构建包含这些状态变化:

  1. flutter 命令先找到当前 Flutter SDK。
  2. SDK 中的 Dart 工具执行 Pub。
  3. Pub 根据 pubspec.yaml、锁文件和 SDK 约束解析依赖。
  4. Pub 写入 .dart_tool/package_config.json,让 Dart 分析器和编译器知道每个包的实际位置。
  5. 如果项目使用生成器,build_runner 读取源码并写出 *.g.dart*.freezed.dart 等文件。
  6. Flutter 编译 Dart 代码,并把资源、平台代码和配置合并到目标平台构建系统。
  7. Android Gradle、Xcode、CMake、Web 构建工具等平台工具生成最终产物。

因此,“能运行 flutter run”至少说明 SDK、依赖解析、Dart 编译和目标平台工具链在当前环境中基本连通;它并不自动说明生产 Flavor、签名、代码生成或发布流程正确。


二、Flutter SDK:不仅是一个命令行程序

2.1 SDK 包含什么

Flutter SDK 通常包含:

  • Flutter 框架源码和 API;
  • 与 Flutter 版本匹配的 Dart SDK;
  • flutter 命令行工具;
  • dart 命令行工具;
  • 引擎二进制和平台构建产物;
  • Flutter 工程模板;
  • 与 Android、iOS、Web、桌面相关的集成脚本。

Flutter 与 Dart 的版本是绑定关系。项目通常不应随意组合一个 Flutter SDK 和另一个独立 Dart SDK。执行:

flutter --version
dart --version
flutter doctor -v

可能看到类似信息:

Flutter 3.x.y • channel stable
Framework • revision ...
Engine • revision ...
Tools • Dart 3.x.y • DevTools ...

具体版本随当前稳定版变化。重要的是确认三件事:

  1. flutterdart 来自期望的 SDK 路径;
  2. Dart 版本满足项目 pubspec.yaml 中的 SDK 约束;
  3. 目标平台所需的外部工具可用。

flutter doctor -v 会检查 Android SDK、Android 工具链、Xcode、CocoaPods、浏览器和桌面编译工具等。它的诊断是环境检查,不是项目构建证明;例如没有 iOS 设备并不意味着 Android 构建一定失败。

2.2 SDK 约束与语言能力

pubspec.yaml 中通常有:

environment:
  sdk: ">=3.2.0 <4.0.0"
  flutter: ">=3.16.0"

这里有两个不同约束:

  • sdk 约束 Dart SDK;
  • flutter 约束 Flutter SDK。

Dart 约束决定项目允许使用哪些语言和核心库能力,例如模式匹配、记录类型、类修饰符等。Flutter 约束决定项目能否使用某些 Flutter API 或构建行为。

约束不是“当前机器版本”,而是“这个项目接受的版本范围”。例如:

environment:
  sdk: ">=3.2.0 <4.0.0"

含义是:

3.2.0VDart<4.0.03.2.0 \leq V_{\text{Dart}} < 4.0.0

其中 VDartV_{\text{Dart}} 是实际使用的 Dart SDK 版本。上限 <4.0.0 防止未来主版本变化后,项目在未验证的情况下自动接受潜在不兼容语言或库行为。

如果 Dart SDK 不满足约束,flutter pub get 会在依赖解析阶段失败,而不是等到运行时失败。常见错误类似:

The current Dart SDK version is 3.1.x.
Because project requires SDK version >=3.2.0 <4.0.0, version solving failed.

恢复方法不是删除 pubspec.lock,而是:

  • 升级或切换 Flutter SDK;
  • 或者根据项目实际代码能力调整 environment.sdk
  • 然后重新运行 flutter pub get 并执行测试。

降低 SDK 约束可能使语法仍能编译,但不代表依赖、分析器和生成器都支持旧版本。

2.3 SDK 版本管理与缓存

团队通常需要固定 SDK 版本,否则同一提交在不同机器上可能出现:

  • Pub 解析到不同依赖版本;
  • 代码生成结果不同;
  • Android Gradle Plugin 或 Xcode 兼容性不同;
  • Flutter 引擎行为不同。

可以直接使用 Flutter SDK 的 channel 和版本机制,也可以使用第三方版本管理器。无论采用哪种方式,核心原则都是:版本选择必须可被 CI 重现。建议在仓库中记录明确的 Flutter 版本,例如通过项目级版本文件或 CI 配置,而不是只写“使用 stable”。

切换 SDK 后应验证:

flutter --version
flutter doctor -v
flutter pub get
flutter analyze
flutter test

flutter clean 只能清理项目构建产物,不能修复 SDK 版本不兼容,也不能替代依赖解析。删除 SDK 缓存可能迫使 Flutter 重新下载引擎和依赖,适用于缓存损坏,但会增加构建时间,不应作为默认排障步骤。

2.4 多平台 SDK 边界

Flutter SDK 本身支持多个目标平台,但每个平台还依赖原生工具:

目标 主要额外工具 典型限制
Android Android SDK、JDK、Gradle/Android 构建工具 不需要 macOS;模拟器和签名另有配置
iOS macOS、Xcode、CocoaPods iOS 构建和签名不能在 Windows/Linux 完成
Web Chrome 或其他浏览器、Web 构建链 不产生原生 APK/IPA
Windows Visual Studio 的桌面 C++ 工具 只能在 Windows 构建 Windows 桌面
macOS Xcode 和 macOS 需要 macOS
Linux GCC、GTK 等开发包 需要 Linux 桌面开发环境

因此,flutter doctor 中的某个平台警告只有在构建该平台时才是阻断问题。例如 Linux 开发者可以正常维护 Android 和 Web 项目,但不能直接完成 iOS 的签名归档。


三、Pub:依赖声明、解析和实际使用之间的关系

3.1 pubspec.yaml 是意图,不是安装目录

Flutter 项目的依赖声明位于 pubspec.yaml

name: toolchain_demo
description: A Flutter toolchain example
publish_to: "none"

environment:
  sdk: ">=3.2.0 <4.0.0"
  flutter: ">=3.16.0"

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.0

各部分含义不同:

  • dependencies:应用运行所需的包;
  • dev_dependencies:开发、测试、代码生成等阶段使用的包;
  • flutter: sdk: flutter:使用当前 Flutter SDK 提供的 Flutter 包;
  • publish_to: none:通常用于不发布到公共 Pub 仓库的应用;
  • environment:约束 Dart/Flutter SDK。

依赖来源可以是:

dependencies:
  http: ^1.2.0
  local_package:
    path: ../local_package
  internal_package:
    git:
      url: https://example.com/internal_package.git
      ref: v1.4.0

常见来源包括:

  • hosted:默认从 Pub 仓库解析;
  • path:从本地路径读取,适合多包仓库或本地联调;
  • git:从 Git 仓库指定分支、标签或提交读取;
  • SDK:从当前 Dart/Flutter SDK 获取。

path 依赖便于调试,但不能假设 CI 上存在同样的相对路径。发布包或共享库时,应避免把开发机路径带入可复现构建。

3.2 Pub 的依赖解析

Pub 需要找到一组版本,使所有约束同时成立。假设:

应用要求:http >=1.0.0 <2.0.0
包 A 要求:http >=1.1.0 <2.0.0
包 B 要求:http >=1.2.0 <1.3.0

交集为:

http >=1.2.0 <1.3.0

因此可以选择 1.2.x 中满足其他条件的版本。

如果约束为:

包 A:http <1.2.0
包 B:http >=1.2.0

交集为空:

(-∞, 1.2.0) ∩ [1.2.0, +∞) = ∅

这时 flutter pub get 会报告 version solving failed。删除锁文件通常不能解决逻辑上的空交集,正确做法是升级、降级或替换冲突依赖,或者使用经过验证的 dependency_overrides 临时覆盖。

3.3 pubspec.lock.dart_tool

执行:

flutter pub get

通常会发生:

  1. 读取 pubspec.yaml
  2. 读取已有 pubspec.lock
  3. 解析所有直接和传递依赖;
  4. 下载缺失包到 Pub 缓存;
  5. 写入或更新 pubspec.lock
  6. 生成 .dart_tool/package_config.json 等项目元数据。

pubspec.lock 记录本次解析实际选中的版本和来源。例如应用锁定了 http 1.2.1,下次只要约束仍满足,Pub 会尽量复用该选择。

通常:

  • Flutter 应用提交 pubspec.lock,保证应用构建可复现;
  • 可发布的 Dart/Flutter 库一般不提交锁文件,让使用者根据库约束解析;
  • .dart_tool 是机器生成目录,通常加入 .gitignore
  • 生成的代码是否提交,取决于团队策略,但 CI 必须能从干净环境稳定生成。

flutter pub getflutter pub upgrade 的区别是:

  • get 优先遵守锁文件;
  • upgrade 会在约束允许范围内重新选择较新的版本,并更新锁文件。

因此依赖升级应单独提交、运行测试并记录变更,而不是在每次构建中无意升级。

3.4 dependency_overrides 的风险

dependency_overrides:
  internal_package:
    path: ../internal_package

覆盖会强行改变整个依赖图对该包的选择。它适合临时联调或验证修复,不适合无说明地长期保留,因为:

  • 可能违反其他包的 API 或版本假设;
  • CI 若没有对应路径会失败;
  • 可能把尚未发布的代码带入生产构建;
  • Pub 的正常兼容性约束被绕过。

排查依赖问题时可以使用:

flutter pub deps
flutter pub outdated

前者查看依赖图,后者查看当前版本、可升级版本和约束阻碍。升级后应重点检查生成器、分析器、平台插件和原生构建工具的兼容性。


四、Flavor:同一应用的多个产品变体

4.1 Flavor 的定义

Flavor 是一个产品变体标识。它通常影响:

  • Android 的 application ID、名称、资源和签名;
  • iOS 的 Bundle Identifier、显示名称、配置文件和签名;
  • Flutter 命令选择的构建变体;
  • 应用内读取的环境配置。

例如:

dev     com.example.app.dev
staging com.example.app.staging
prod    com.example.app

Flavor 不是简单的字符串替换,也不是 Dart 原生语言概念。Flutter 命令只是把 --flavor 传给底层平台构建系统,并在 Flutter 层提供当前 flavor 信息。真正可用的变体必须先在 Android 或 iOS 工程中配置。

4.2 Android Flavor 的构建路径

在 Android 模块的 Gradle 配置中可以定义:

android {
    flavorDimensions += "environment"

    productFlavors {
        create("dev") {
            dimension = "environment"
            applicationIdSuffix = ".dev"
            resValue("string", "app_name", "Demo Dev")
        }
        create("staging") {
            dimension = "environment"
            applicationIdSuffix = ".staging"
            resValue("string", "app_name", "Demo Staging")
        }
        create("prod") {
            dimension = "environment"
            resValue("string", "app_name", "Demo")
        }
    }
}

这里的关键因果关系是:

  1. productFlavors 定义 Android 变体;
  2. applicationIdSuffix 改变安装包标识,使开发版和生产版可以同时安装;
  3. resValue 为不同变体生成不同 Android 资源;
  4. --flavor dev 选择对应变体。

命令示例:

flutter run --flavor dev -t lib/main_dev.dart
flutter build apk --flavor staging -t lib/main_staging.dart
flutter build appbundle --release --flavor prod -t lib/main_prod.dart

如果未定义 dev,常见失败表现是 Gradle 报告找不到对应 variant 或 flavor。此时应先检查 Gradle 配置和名称大小写,而不是尝试增加 --dart-define=FLAVOR=dev;后者不会创建 Android product flavor。

Android 的变体还受 build type 影响,例如:

devDebug
devRelease
prodDebug
prodRelease

Flavor 只解决“产品维度”,debug/release 解决“构建类型”。两者组合后才是实际 Gradle variant。

4.3 iOS Flavor 的构建路径

iOS 没有 Android 同名的 productFlavors 模型。通常使用:

  • Xcode targets 或 schemes;
  • Build Configurations;
  • .xcconfig 文件;
  • 不同 Bundle Identifier;
  • 不同签名和 entitlements。

一种常见结构是:

Debug-Dev.xcconfig
Release-Dev.xcconfig
Debug-Prod.xcconfig
Release-Prod.xcconfig

然后在 Xcode scheme 中把 scheme 映射到相应 configuration,并设置不同的 Bundle Identifier、显示名称、资源和签名设置。

构建时:

flutter run --flavor dev -t lib/main_dev.dart
flutter build ipa --release --flavor prod -t lib/main_prod.dart

这里 --flavor 对 iOS 的实际作用依赖 Xcode 工程中存在同名 scheme。若 scheme 未创建或未共享,CI 可能出现本机可构建、CI 找不到 scheme 的情况。将 scheme 设置为 Shared,并把相关 Xcode 工程配置提交到仓库,是可验证构建的必要条件。

iOS 的签名还受以下因素影响:

  • Apple Developer Team;
  • Bundle Identifier;
  • provisioning profile;
  • certificates;
  • entitlements;
  • Xcode 和 macOS 环境。

所以 Flavor 选择正确不等于 IPA 可以签名归档。

4.4 Web 和桌面的 Flavor 差异

Flutter Web、Windows、macOS、Linux 没有 Android product flavor 或 iOS scheme 这套统一原生机制。可以使用:

flutter build web \
  --dart-define=APP_ENV=staging \
  -t lib/main_staging.dart

桌面应用也可以使用不同入口文件、编译定义、原生工程配置或构建脚本实现变体。

因此应区分:

需求 Android/iOS 常用机制 Web/桌面常用机制
选择 Dart 入口 -t -t
选择平台产品变体 product flavor / scheme 构建脚本或原生工程配置
传入 Dart 配置 --dart-define --dart-define
修改包标识 application ID / Bundle ID 平台原生工程标识
签名 Android keystore / Apple signing 平台各自签名机制

不要把 --flavor 当作所有平台都存在的统一产品系统。跨平台项目通常将“平台产品变体”和“Dart 环境值”分层管理。


五、使用入口文件和配置对象组织 Flavor

可以为每个环境提供入口文件:

// lib/main_dev.dart
import 'app.dart';
import 'config/app_config.dart';

void main() {
  runApp(App(config: AppConfig.dev()));
}
// lib/main_prod.dart
import 'app.dart';
import 'config/app_config.dart';

void main() {
  runApp(App(config: AppConfig.prod()));
}

不过如果环境数量增多,完全复制入口文件会产生重复。更通用的方式是统一入口,通过编译期变量决定配置:

import 'package:flutter/widgets.dart';
import 'app.dart';
import 'config/app_config.dart';

const _environment =
    String.fromEnvironment('APP_ENV', defaultValue: 'dev');

void main() {
  final config = AppConfig.fromEnvironment(_environment);
  runApp(App(config: config));
}

配置类应限制可用值:

class AppConfig {
  const AppConfig({
    required this.environment,
    required this.apiBaseUrl,
    required this.enableDebugPanel,
  });

  final String environment;
  final Uri apiBaseUrl;
  final bool enableDebugPanel;

  factory AppConfig.fromEnvironment(String value) {
    switch (value) {
      case 'dev':
        return const AppConfig(
          environment: 'dev',
          apiBaseUrl: Uri.parse('https://dev-api.example.com'),
          enableDebugPanel: true,
        );
      case 'staging':
        return const AppConfig(
          environment: 'staging',
          apiBaseUrl: Uri.parse('https://staging-api.example.com'),
          enableDebugPanel: true,
        );
      case 'prod':
        return const AppConfig(
          environment: 'prod',
          apiBaseUrl: Uri.parse('https://api.example.com'),
          enableDebugPanel: false,
        );
      default:
        throw ArgumentError.value(
          value,
          'APP_ENV',
          'must be dev, staging, or prod',
        );
    }
  }

  const AppConfig.dev()
      : this(
          environment: 'dev',
          apiBaseUrl: Uri.parse('https://dev-api.example.com'),
          enableDebugPanel: true,
        );

  const AppConfig.prod()
      : this(
          environment: 'prod',
          apiBaseUrl: Uri.parse('https://api.example.com'),
          enableDebugPanel: false,
        );
}

构建命令:

flutter run --dart-define=APP_ENV=dev
flutter build apk --release --dart-define=APP_ENV=prod

这里有一个容易忽略的事实:String.fromEnvironment 读取的是编译环境声明,不是操作系统运行时环境变量。必须在 flutter runflutter build 命令中传入;应用安装到手机后,不能通过修改手机 shell 环境变量改变它。

bool.fromEnvironment 可以读取布尔编译常量,但配置值多时,统一读取字符串后由配置类校验,通常更容易发现拼写错误。错误环境值应尽早失败,而不是静默回退到生产地址或开发地址。


六、环境配置:编译期值、运行时值和秘密的边界

6.1 --dart-define 的性质

Dart 编译期常量可以这样定义:

const apiUrl = String.fromEnvironment(
  'API_URL',
  defaultValue: 'https://dev-api.example.com',
);

const isLoggingEnabled = bool.fromEnvironment(
  'ENABLE_LOGGING',
  defaultValue: false,
);

命令:

flutter run \
  --dart-define=API_URL=https://dev-api.example.com \
  --dart-define=ENABLE_LOGGING=true

更复杂的值可以使用文件形式:

flutter build web \
  --dart-define-from-file=config/staging.json

示例 config/staging.json

{
  "APP_ENV": "staging",
  "API_URL": "https://staging-api.example.com",
  "ENABLE_LOGGING": "true"
}

--dart-define-from-file 的具体支持范围和参数细节应以当前 SDK 的 flutter build/run --help 为准。无论命令形式如何,核心语义都是把值作为编译期定义传给 Dart,而不是在运行时读取 JSON 文件。

6.2 编译期配置不是秘密存储

移动端、Web 和桌面客户端最终都把代码和配置交付给用户。即使配置通过 --dart-define 传入,也可能被:

  • 从 Web JavaScript 产物中搜索;
  • 从 APK/IPA 或桌面二进制中分析;
  • 通过调试、日志或网络行为推断。

因此以下内容不能放进客户端配置:

  • API 私钥;
  • 数据库密码;
  • 云服务管理凭证;
  • 签名私钥;
  • 具有后台管理权限的 token。

客户端可以携带公开的 API 地址、公开 client ID、非敏感功能开关,但真正的秘密必须放在服务端或受控的构建签名系统中。构建系统中的环境变量也要避免在日志中打印。

6.3 运行时配置

如果希望应用安装后无需重新构建即可修改配置,可以在启动阶段读取远程配置或本地文件:

Future<AppConfig> loadRuntimeConfig() async {
  final response = await fetchConfig();
  return AppConfig.fromJson(response);
}

这会引入新的状态和故障路径:

sequenceDiagram
    participant Main as main()
    participant Store as 本地缓存
    participant Server as 配置服务
    participant App as Flutter 应用

    Main->>Store: 读取最近一次有效配置
    alt 本地配置有效
        Store-->>Main: 返回缓存配置
    else 无缓存或已过期
        Main->>Server: 请求运行时配置
        alt 请求成功且校验通过
            Server-->>Main: 返回配置
            Main->>Store: 持久化配置
        else 请求失败或签名无效
            Main->>Main: 使用安全默认值或终止启动
        end
    end
    Main->>App: 注入不可变配置

运行时配置必须考虑:

  • 网络不可用;
  • 返回格式错误;
  • 版本不兼容;
  • 配置被篡改;
  • 远程服务返回错误值;
  • 本地缓存过期。

对于 API 地址这类基础配置,常见方案是编译期确定;对于可动态关闭的功能开关,可以使用运行时配置,但应有安全默认值和回滚策略。不能让一次错误的远程配置把所有客户端推向不可用服务。


七、代码生成:从源码到生成文件

7.1 为什么需要生成

Dart 的反射在 Flutter 中不是通用的工程方案,尤其在 AOT、Tree Shaking 和跨平台构建中,运行时反射会增加复杂性。因此很多项目通过代码生成实现:

  • JSON 序列化;
  • 不可变数据类;
  • Union/sealed 状态类;
  • 路由注册;
  • 依赖注入注册;
  • 数据库表或 schema 映射。

代码生成的本质是一个函数:

G(S,C)OG(S, C) \rightarrow O

其中:

  • SS 是源码输入;
  • CC 是生成器配置和依赖版本;
  • OO 是生成的 Dart 输出。

只要 SSCC 变化,输出 OO 可能变化。因此生成代码不能脱离生成器版本和输入源码单独理解。

7.2 build_runnerjson_serializable 示例

pubspec.yaml

dependencies:
  json_annotation: ^4.9.0

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.8.0

源码:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  const User({
    required this.id,
    required this.name,
  });

  final int id;
  final String name;

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

运行:

dart run build_runner build --delete-conflicting-outputs

在 Flutter 项目中也可以使用:

flutter pub run build_runner build --delete-conflicting-outputs

现代 Dart 工具链更推荐 dart run 形式。命令成功后会生成 user.g.dart,其中包含 _$UserFromJson_$UserToJson 的实现。

测试代码:

void main() {
  final user = User.fromJson({
    'id': 7,
    'name': 'Ada',
  });

  assert(user.id == 7);
  assert(user.toJson()['name'] == 'Ada');
}

每一步成立的原因是:

  1. part 'user.g.dart'; 把生成文件纳入同一个 Dart library;
  2. @JsonSerializable() 告诉生成器处理 User
  3. 工厂方法引用生成函数;
  4. 生成器根据字段类型产生 JSON 转换;
  5. 编译器检查生成函数的名称和类型是否匹配。

如果忘记 part,生成文件即使存在也不会成为该 library 的一部分;如果类名、文件名或生成函数引用不匹配,常见错误是:

Undefined function '_$UserFromJson'

如果手工修改了 user.g.dart,下一次生成通常会覆盖修改。生成文件应视为构建产物,修复应放在输入源码、注解或生成器配置中。

7.3 生成冲突和 --delete-conflicting-outputs

当输出文件已存在但不符合当前生成器的预期,build_runner 可能报告冲突。:

dart run build_runner build --delete-conflicting-outputs

这个参数允许工具删除冲突的生成输出再重新生成。它不会删除任意业务源码,但仍应确认生成目录和 build.yaml 配置正确。错误的输出路径配置可能导致有价值的手写文件被当成生成输出处理。

开发阶段可以持续监听:

dart run build_runner watch --delete-conflicting-outputs

watch 会在输入文件变化后重新生成;CI 和发布流程不应依赖一个长期运行的 watch 进程,而应执行一次干净的 build,这样缺失生成文件会立即暴露。

7.4 代码生成与版本一致性

生成器往往依赖:

  • build_runner
  • source_gen
  • 注解包;
  • analyzer
  • Dart SDK 语言版本。

升级其中一个包可能造成:

  • 生成 API 名称变化;
  • 分析器不兼容;
  • 生成文件格式变化;
  • 大量无业务意义的 diff;
  • 编译器出现类型错误。

推荐在 CI 中使用:

flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter analyze
flutter test

如果团队提交生成文件,CI 可以检查生成前后的 Git 工作区是否干净:

git diff --exit-code

这能发现开发者修改了注解但没有提交重新生成的结果。若团队不提交生成文件,则 CI 必须在编译前生成,且发布构建不能跳过该步骤。

7.5 build.yaml 的作用

生成器默认行为可以通过 build.yaml 调整,例如输出目录、启用的 builder 或选项。配置应与实际包名和生成器文档一致,不能凭记忆编造 builder 名称。排查生成问题时按以下顺序检查:

  1. pubspec.yaml 是否同时包含运行时注解包和开发期生成器;
  2. part 文件名是否正确;
  3. 输入类上的注解是否正确;
  4. 生成器版本是否满足 Dart/Flutter SDK;
  5. build.yaml 是否改变了默认输入输出;
  6. 是否有旧输出导致冲突;
  7. 是否在正确的包根目录执行命令。

多包仓库中,通常应在包含目标 pubspec.yaml 的包目录运行生成命令,而不是随意在仓库根目录运行。


八、把 Flavor、代码生成和环境配置组合起来

一个可维护的构建矩阵应明确每个维度:

平台 × 产品环境 × 构建类型
Android × dev     × debug
Android × staging × release
Android × prod    × release
iOS     × dev     × debug
Web     × staging × release

不要让一个变量同时承担多个含义。例如:

  • --flavor prod:选择 Android/iOS 产品变体;
  • --dart-define=APP_ENV=prod:传入 Dart 编译期环境;
  • --release:选择优化和调试信息策略;
  • -t lib/main_prod.dart:选择 Dart 入口。

可以在 CI 中显式绑定它们:

set -e

flutter pub get
dart run build_runner build --delete-conflicting-outputs

flutter build appbundle \
  --release \
  --flavor prod \
  -t lib/main.dart \
  --dart-define-from-file=config/prod.json

flutter analyze
flutter test

实际项目中应先测试再构建,或者使用更严格的流水线:

set -e

flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter analyze
flutter test
flutter build appbundle --release --flavor prod \
  --dart-define-from-file=config/prod.json

这里的顺序不是绝对规范,但有明确取舍:

  • 先生成,保证分析和测试看到最新代码;
  • 先分析和测试,避免把明显错误带入较慢的 release 构建;
  • 生产构建使用明确的 flavor、入口和配置文件,不依赖开发机当前状态。

对于 Android,可以进一步验证产物:

flutter build appbundle --release --flavor prod

预期会在 build/app/outputs/bundle/prodRelease/ 附近产生 AAB,具体路径可能随 Flutter/Gradle 工程版本变化。不要只依据命令退出码判断包内容正确,还应检查:

  • application ID 是否为生产值;
  • 版本号和版本名称是否正确;
  • 使用的 API 地址是否正确;
  • 生产包是否错误包含调试入口;
  • 签名和 release 配置是否生效。

九、常见失败表现与诊断路径

9.1 flutter pub get 失败

优先区分三类原因:

  1. SDK 约束不满足
    检查 flutter --versionenvironment.sdk

  2. 版本约束无交集
    查看错误中指出的直接或传递依赖,使用 flutter pub depsflutter pub outdated 分析。

  3. 网络或仓库问题
    检查 Pub 仓库地址、代理、缓存和 Git 依赖可访问性。网络错误不是通过修改版本约束解决的。

pubspec.lock 只有在锁定结果损坏、需要重新解析或升级依赖时才应删除。删除后可能得到一批新版本,必须重新测试。

9.2 生成文件缺失

表现通常是:

Target of URI doesn't exist: 'model.g.dart'
Undefined name '_$ModelFromJson'

诊断顺序:

dart run build_runner build --delete-conflicting-outputs
flutter analyze

如果生成命令本身失败,先修生成器错误;如果生成成功但分析器仍报错,检查 part 路径、包目录和文件是否被 .gitignore 或 IDE 排除。

9.3 Flavor 找不到

Android 侧检查:

  • Gradle 是否声明了目标 flavor;
  • flavor 是否属于正确的 dimension;
  • 命令中的名称是否大小写一致;
  • 当前工程是否真的包含 Android 模块。

iOS 侧检查:

  • Xcode 是否存在同名 scheme;
  • scheme 是否 Shared;
  • configuration 是否映射到正确的 .xcconfig
  • Bundle Identifier 和签名是否对应该环境。

Web/桌面侧则不应期待平台自动识别 Android 风格 flavor,应检查入口文件和 --dart-define

9.4 配置值没有生效

常见原因包括:

  • --dart-define 写在错误的命令层级;
  • 使用了 String.fromEnvironment 却忘记声明 const
  • 修改了配置文件但没有重新构建;
  • 运行的是旧安装包;
  • Flavor 选择的是 staging,但 Dart 配置传的是 prod;
  • 使用 --dart-define-from-file 的 SDK 不支持该参数或文件格式不符合要求。

可以在非生产构建中输出经过脱敏的环境名和 API 主机名,验证构建输入;不要打印 token、密码或完整授权头。


十、工程边界和生产取舍

10.1 Flavor 与后端环境不是天然一一对应

prod Flavor 可以指向 staging API,技术上完全可能;这也正是风险所在。Flavor 名称只是构建标识,真正的后端地址来自配置。应在构建验证中同时检查:

Flavor = prod
APP_ENV = prod
API_URL = production endpoint
Bundle ID = production identifier
Signing = production signing

如果只验证其中一项,就可能生成“生产包名但连接测试服务”或“测试包名却连接生产服务”的错误产物。

10.2 不要把 Debug/Release 当作环境

Debug 和 Release 是构建类型,通常影响:

  • 优化;
  • 调试器;
  • 断言;
  • 日志;
  • 签名;
  • 原生编译设置。

Dev、Staging、Prod 是产品或部署环境。一个项目完全可以存在:

devDebug
devRelease
prodDebug
prodRelease

是否允许 prodDebug 要由发布策略决定,但概念上它并不等于 devRelease

10.3 生成代码不是源代码真相

源代码、注解和 schema 是输入;生成文件是派生产物。发生冲突时应修改输入并重新生成。将生成文件和手写逻辑混在同一个文件中,会使覆盖、审查和增量生成变得危险。

如果生成结果很大,提交还是不提交没有绝对规范:

  • 提交生成文件:检出后更快看到完整源码,构建可少一个生成步骤,但需要严格同步检查;
  • 不提交生成文件:仓库更简洁,但所有开发和 CI 都必须可靠执行生成;
  • 无论哪种方式,都要锁定生成器依赖并验证干净构建。

10.4 Web、桌面和移动端不能共用所有配置假设

移动端通常有原生包标识、签名和商店变体;Web 没有 APK/IPA,也没有同样的安装包隔离;桌面平台的签名、安装器和更新机制又各不相同。

因此共享的应是:

  • Dart 配置模型;
  • 环境枚举;
  • API 客户端;
  • 校验逻辑;
  • 测试。

不应强行共享:

  • Android Gradle flavor 配置;
  • iOS scheme 配置;
  • Web 的静态资源发布逻辑;
  • Windows/macOS/Linux 的签名脚本。

十一、一个最小可复现工作流

新机器或 CI 从干净目录开始,可以按以下顺序执行:

flutter --version
flutter doctor -v

flutter pub get

dart run build_runner build --delete-conflicting-outputs

flutter analyze
flutter test

flutter run \
  --flavor dev \
  -t lib/main.dart \
  --dart-define=APP_ENV=dev

每一步的验证目标是:

  1. flutter --version:确认 SDK 版本和 Dart 版本;
  2. flutter doctor -v:确认目标平台原生工具;
  3. flutter pub get:确认依赖约束和缓存可用;
  4. build_runner build:确认派生源码完整;
  5. flutter analyze:确认静态类型和生成 API 一致;
  6. flutter test:确认业务行为;
  7. flutter run:确认平台 Flavor、Dart 配置和运行时启动路径一致。

当这条路径稳定后,再加入 release 构建、签名、归档、商店上传、灰度和回滚。发布系统中的每个变量都应能追溯到具体 SDK 版本、锁定依赖、生成结果、Flavor、配置文件和签名配置;否则构建成功仍可能得到错误的应用变体。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。