Flutter 老项目升级:从 Dart 2 / 旧 Gradle 到 Flutter SDK 3.38
前言
把几年前的 Flutter 工程拉到 Flutter 3.38.x(Dart 3.10+) 上,往往不是「改一行 SDK 版本」就结束:依赖要满足 Dart 3;Android 要跟上 AGP 8 + 新 Flutter Gradle 插件;iOS 则要关注 部署版本、CocoaPods、Xcode 与隐私清单。老插件在 Android 侧常卡在 namespace,在 iOS 侧则可能是 Pod 最低系统版本 / Swift 与工程设置不一致。下面按实践整理检查清单与建议。
一、先定「目标工具链」,再动 pubspec
1. environment.sdk 是硬门槛
老项目常见写法:
environment:
sdk: '>=2.19.6 <3.0.0'
在 Dart 3 下会直接无法解析依赖。应改为与当前 Flutter 自带的 Dart 对齐,例如:
environment:
sdk: ^3.10.0
要点:以本机 flutter --version 里的 Dart 版本为准;^3.10.0 表示「至少 3.10,小于 4.0」,与 Flutter stable 通常一致。
2. 依赖升级:「能 resolve」≠「能编译」
flutter pub outdated 里 Resolvable 一列是重要参考,但还要注意:
| 情况 | 建议 |
|---|---|
主版本升级(如 flutter_bloc 8→9) | 看 changelog,重点看 破坏性变更 |
分析器报 depend_on_referenced_packages | 对「直接 import 的包」在 pubspec 里 显式声明(例如单独依赖 bloc) |
某包只在代码里 import 但未声明 | 补上依赖,避免后续 CI / 严格分析失败 |
3. flutter_lints 放哪里
flutter_lints 应放在 dev_dependencies,不要和运行时依赖混在一起;升级时可顺带升到与模板接近的版本(如 ^6.0.0),再逐步消化新 lint。
二、Android:从「apply flutter.gradle」到「Flutter Gradle Plugin」
1. 为什么要改
较新的 Flutter 模板使用:
-
settings.gradle里的 pluginManagement +dev.flutter.flutter-plugin-loader -
app/build.gradle里id "dev.flutter.flutter-gradle-plugin" -
AGP 8.x 要求 library 模块声明
namespace,老插件常在这里翻车
旧工程典型特征:apply from: "$flutterRoot/.../flutter.gradle"、Gradle 7.x、Kotlin 1.7 等。
2. 版本对齐思路
不必自己猜数字,以 当前 Flutter SDK 里 gradle_utils.dart 的模板常量(或 flutter create 生成的新工程)为参照,例如:
| 项 | 典型取向(随 Flutter 版本变化,以你本机 SDK 为准) |
|---|---|
| Gradle | 8.12~8.14 一带 |
| Android Gradle Plugin | 8.11.x 一带 |
| Kotlin | 2.2.x 一带 |
| Java | 17(compileOptions / jvmTarget) |
3. app 模块必做
-
在
android { namespace "..." }与 Manifest 的package一致或按迁移文档处理 -
compileSdk/minSdk/targetSdk交给flutter.compileSdkVersion等扩展字段,减少与引擎脱节
三、iOS:Podfile、部署目标与 Xcode
iOS 没有 Android 的 AGP namespace 问题,但升级时常见坑在 CocoaPods 环境、最低系统版本、Xcode 与苹果合规。可与 Android 并行排查。
1. Podfile 里的 platform :ios
platform :ios, '13.0'
| 注意点 | 说明 |
|---|---|
| 全局最低版本 | 需 不低于 Flutter 引擎与插件 Podspec 的要求;版本过低时,某个 Pod 会在 pod install 或编译期报错 |
| 与 Xcode 一致 | Runner 的 Deployment Target、project.pbxproj 中的 IPHONEOS_DEPLOYMENT_TARGET 应与 Podfile 思路一致;post_install 里 flutter_additional_ios_build_settings 会帮各 target 对齐一部分编译选项,但 platform 仍是总闸门 |
| 逐步提高 | 从 iOS 11/12 往上提到 13+ 往往更稳;本仓库当前为 13.0 |
升级后若编译报「某 framework 需要 iOS xx+」,优先 提高 platform :ios 或 升级对应插件。
2. CocoaPods 与终端环境
| 步骤 | 说明 |
|---|---|
| 顺序 | 先 flutter pub get,再 cd ios && pod install(或直接 flutter build ios,由工具链触发) |
| UTF-8 | 部分环境下 CocoaPods 会因编码报错(如 Unicode Normalization ... ASCII-8BIT),执行前设置:export LANG=en_US.UTF-8(可写入 ~/.zshrc) |
| 清理 | 异常时可 flutter clean,必要时删除 ios/Pods、ios/Podfile.lock 后重装(团队项目改 lock 需与协作约定一致) |
export LANG=en_US.UTF-8
cd ios
pod install --repo-update # 网络允许时可选,更新 spec
3. Xcode 版本
-
以 Flutter 官方文档当前 stable 要求的 Xcode 最低版本为准;Xcode 过旧会遇到 Swift 版本、SDK API、链接器 一类错误。
-
首次用新版本 Xcode 打开
ios/Runner.xcworkspace(注意是 workspace,不是仅xcodeproj)。
4. Info.plist、权限文案与隐私清单
| 类型 | 说明 |
|---|---|
| Usage Description | 使用相册、相机、麦克风、定位、蓝牙等时,必须在 Info.plist 配置对应 NS*UsageDescription,否则运行期可能直接崩溃或审核被拒 |
| ATS / 明文 HTTP | 若仍访问 http,需检查 App Transport Security 例外配置是否符合产品安全要求 |
| Privacy Manifest | 苹果对第三方 SDK 的 隐私清单要求随时间收紧;升级插件后若 Xcode / App Store Connect 出现新的 Privacy 相关告警,需按插件文档补充 PrivacyInfo.xcprivacy 或升级插件版本 |
Dart 层无改动的「纯升级」也可能因 新引擎 / 新插件 触发上述检查,建议在真机或 TestFlight 走一遍核心功能。
5. 签名、Capabilities 与扩展
-
打开 Xcode 检查 Signing & Capabilities、Bundle Identifier、Team 是否仍有效。
-
若使用 Push、Associated Domains、Keychain Sharing 等,升级后确认 entitlements 未丢失或与证书匹配。
6. 与 Android 的对比(心理预期)
| 维度 | Android | iOS |
|---|---|---|
| 典型构建错误 | Gradle / AGP / namespace | Pod 解析失败、部署版本过低、Swift / 链接 |
| 依赖管理 | Maven + Gradle | CocoaPods(由 Flutter 聚合插件 Pod) |
| 快速无签名验证 | flutter build apk | flutter build ios --no-codesign(适合 CI 验证能否编过) |
7. iOS 侧验收建议
flutter build ios --no-codesign
通过后再在本机 Xcode Archive 或 flutter run 真机 验证签名与运行时权限。
四、插件与传递依赖:两类「升级杀手」
1. AGP 8:namespace 缺失
报错里若出现 「Namespace not specified」 且指向 .pub-cache/.../某插件/android/build.gradle,说明该插件 Android 部分过旧。
| 策略 | 适用 |
|---|---|
| 换维护中的替代方案 | 功能可用 官方或社区活跃包 替代(如状态栏用 SystemChrome + SystemUiOverlayStyle) |
| 临时 Gradle 注入 namespace | 仅适合你能维护的 fork,不推荐长期依赖魔法补丁 |
| 锁旧 AGP | 与 Flutter 3.38 推荐栈冲突,不推荐 |
本仓库示例:status_bar_control 因 Gradle 过旧且无 namespace,已改为 SystemChrome 在 Dart 侧实现等价效果,并移除该插件。
2. Dart SDK 与传递包不兼容(例:win32)
编译期若在 .pub-cache/.../win32/... 报 类型找不到(例如与 typed_data / Uint8List 相关 API 变迁),往往是 传递依赖版本过旧。
| 做法 | 说明 |
|---|---|
dependency_overrides 强制新版本 | 快速 unblock;注明注释,待上游升级后再删 |
flutter pub upgrade 相关链 | 有时能自然拉高 win32,可优先试 |
本仓库示例:在 pubspec.yaml 中使用 dependency_overrides: win32: ^5.10.1,让解析器拿到与 Dart 3.10 匹配的 win32。
五、框架与 API 迁移:建议按「弃用提示」分批改
1. WillPopScope → PopScope
Flutter 3.12+ 起 WillPopScope 弃用,与 Android 预测性返回 相关。迁移要点:
-
使用
PopScope(canPop: ..., onPopInvokedWithResult: ...) -
原「二次返回退出」等逻辑,在
didPop == false分支里自行Navigator.pop或exit(0) -
iOS 上若原本不拦截返回,可保留「直接
child、不包PopScope」的分支,避免行为回退
2. MediaQuery.textScaleFactor → textScaler
系统字体缩放相关 API 已迁到 TextScaler,例如固定为不随系统缩放:
MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(1.0))
3. Dio 5:DioError → DioException
on DioError catch 与拦截器里的类型都应改为 DioException;超时判断宜用 DioExceptionType,而不是依赖 TimeoutException 的 is 判断(类型上往往对不上)。
4. logger 2.x:日志级别方法更名
分析器会提示:v → t(trace),wtf → f(fatal)。属于机械替换,适合一次性改完。
5. 路由参数与 Dart 3 类型
as Map? + 动态下标在严格模式下容易埋雷。更稳妥的写法是:
final raw = context?.settings?.arguments;
final map = raw is Map<String, dynamic> ? raw : null;
final id = map?['entityId'] as int? ?? 0;
六、验证顺序:比「能 analyze」更重要的是「能产物」
建议顺序:
| 步骤 | 目的 |
|---|---|
flutter pub get | 依赖可解析 |
dart analyze / flutter analyze | 消灭 error;warning / info 可分阶段 |
flutter build apk --debug | Android 整条链(Gradle + 插件 + Dart 编译) |
flutter build ios --no-codesign | iOS 编译链(Pod + Xcode 工程,不要求当时具备签名) |
flutter test | 至少有一个不依赖全 App 初始化的测试,避免模板测试长期失效 |
iOS:pod install + 真机 / Archive | LANG=UTF-8、Xcode 打开 .xcworkspace |
export LANG=en_US.UTF-8 && cd ios && pod install
七、实践注意点(速查表)
| 现象 | 排查 / 建议 |
|---|---|
| Gradle 报某插件 namespace | 升级插件或替换实现;避免长期钉死旧 AGP |
| Dart 编译报 pub-cache 里第三方包 类型错误 | dependency_overrides 或升级引入该包的直接依赖 |
WillPopScope 弃用告警 | 迁 PopScope |
| 字体缩放相关弃用 | textScaler |
| 网络层 Dio 告警 | DioException + DioExceptionType |
测试里 pumpWidget(MyApp()) 失败 | main() 里有 异步初始化 / 多 Provider 时,测试要 mock 或 简化入口 |
pod install 编码 / normalize 报错 | 终端 export LANG=en_US.UTF-8(或 LC_ALL) |
| 编译提示 iOS 版本过低 | 提高 Podfile platform,并与 Xcode Deployment Target 对齐 |
| 真机权限崩溃 | 补全 Info.plist 各类 NS*UsageDescription |
八、小结
| 主题 | 核心建议 |
|---|---|
| Dart / SDK | environment.sdk 升到 Dart 3,与 Flutter 版本绑定思考 |
| 依赖 | 主版本看 changelog;直接 import 的包要 显式依赖;lint 放 dev_dependencies |
| Android | 跟随 Flutter 模板:新 settings + Flutter Gradle Plugin + AGP 8 + Java 17 |
| iOS | platform :ios、pod install(UTF-8)、Xcode 版本、Info.plist / 隐私清单;用 flutter build ios --no-codesign 做编译验收 |
| 老插件 | Android:namespace / 旧 Gradle;iOS:Pod 最低版本 / Swift;优先 换插件或升级 |
| 传递依赖 | 遇 win32 类 SDK 不兼容,可用 dependency_overrides 临时解决并留注释 |
| 业务代码 | PopScope、TextScaler、DioException、logger API 为高概率改动点 |
| 验收 | flutter build apk +(如有 iOS)flutter build ios --no-codesign;analyze 通过不等于能发版 |
升级时可以先问自己:Dart 3 能否过?Android Gradle / namespace 能否过?iOS Pod 与部署版本能否过?有没有「弃用 API + 死插件」要替换? 按上表逐项划掉,大部分老项目都能稳定落到当前 stable 工具链上。