1. 环境搭建之前,先把“搭的是什么”想明白
1.1 一次flutter run背后到底经过了多少只手
每次带新同事,第一步都是 Flutter 环境搭建。大部分人把它理解成“装个 Flutter,跑个 demo”,结果装完之后发现flutter doctor一屏红叉,然后就开始按报错顺序乱点,越点越乱。问题的根源不在于操作复杂,而在于没搞清楚这条链路上到底有几个环节在协作。
一条完整的运行链路是这样的:你写的 Dart 代码先由 Flutter 自带的 Dart 编译器处理成可执行产物,同时 Flutter 框架层把 Dart 的 Widget 树翻译成各平台原生的绘制指令;到了 Android 侧,还需要 JDK 提供 Java 运行环境、Gradle 负责依赖解析与打包、Android SDK 提供编译所需的平台与构建工具;到了 iOS 侧,则需要 Xcode 的命令行工具链和 CocoaPods 管理原生依赖。也就是说,Flutter 只是这条链路的“上游”,它自己并不生产最终的应用包,最终还是要交给各平台的原生工具链去打包。
想清楚这一点,很多所谓的“环境问题”就有了解释:flutter doctor报的错,绝大多数不是 Flutter 本身的问题,而是它探测到了下游工具链缺件,比如缺 JDK、缺 Android SDK、缺 Xcode license。所以正确的心态是把它当成一个“体检报告”,从下往上修,而不是从上往下修。我在实际项目里带的几个组,都是先让他们把下游装齐,再回头装 Flutter,doctor一次全绿的概率高得多。
还有一点容易被忽略:Flutter 的 SDK 本身是自包含的,它内部绑定了特定版本的 Dart 编译器、打包脚本、以及一堆模板文件。这意味着升级 Flutter 往往会顺带改变 Dart 版本和构建模板,进而影响既有工程的构建结果。所以环境搭建不只是“装上”,还得考虑“以后怎么换版本”。这是很多人搭完环境三个月后才开始后悔的地方,那时候一堆工程已经建好了,切版本代价陡增。
适合看这篇内容的人,我大致分成三类:完全没接触过 Flutter、想从零搭一套能长期用的环境的;装过但一直有红叉、跑不起来 demo 的;以及环境能跑但每次依赖同步、切换分支都要重装一遍、效率极低的。这三类的处理路径其实不一样,后面我会分别标出来。
1.2 版本策略:为什么不建议一上来就追最新
先说一个反直觉的观点:环境搭建阶段,选版本的第一原则是“能被复现”,不是“足够新”。很多人第一次装就下载最新版本,然后照着一年前的教程配置,命令对不上、参数名改了、目录结构也变了,最后怀疑人生。
我的做法是分两层考虑。第一层是渠道选择,Flutter 提供多个发布渠道,日常开发用正式发布渠道就够了,试验性功能才去碰 beta 或 dev 渠道,而且只在独立目录里碰,绝不和主环境混在一起。第二层是具体版本号,如果你要维护已有工程,就去看那个工程pubspec.yaml里声明的 SDK 约束,以及工程根目录下记录的版本信息,照着装同一个大版本;如果是全新项目,选当前正式渠道的最新版本即可,但要在搭建完成后立刻把版本号记到一份个人环境清单里,后面所有机器都对齐它。
为什么不建议“装最新的就行”还有一个现实原因:Flutter 版本和 Android 的构建工具版本、JDK 版本之间存在兼容矩阵。新版本 Flutter 往往要求较新的 JDK,而较新的 JDK 又可能和旧工程里的 Gradle 版本打架。你现在装个最新的,三个月后接一个老工程,就得再装一遍旧版本。与其到时候折腾,不如一开始就把“多版本共存”这条路留出来。
留路的方式很简单:不要用安装器把 Flutter 装到系统默认位置,而是手动下载压缩包,解压到一个你自己管理的目录下,一个版本一个文件夹。比如在一个统一的工作目录下建sdk/flutter-3.x-a、sdk/flutter-3.x-b这样的结构,切版本时改一个环境变量或者用版本管理工具指过去就行,全程不重装、不覆盖。这个习惯我坚持了好几年,带人时也一直这么要求,省下来的时间非常可观。
2. 核心组件逐项拆解与配置要点
2.1 Flutter SDK 的下载与目录规划:路径里别留空格和中文
Flutter SDK 本质就是一个压缩包,解压即用,不需要安装。听起来最省事的一步,恰恰是踩坑率最高的地方,罪魁祸首几乎都是路径。
我把这条当成铁律:Flutter SDK 的存放路径、工程路径、以及所有相关工具的安装路径,全部不允许出现空格和中文。原因不是 Flutter 不支持,而是它调用的下游工具链里有些老旧的脚本对路径转义处理不严谨,一旦路径含空格,就容易在构建过程中被截断成两段,报出来的错还完全不相干,比如“找不到某个文件”,而你去看那个文件明明在。这类问题排查起来极其费时,因为报错信息和真实原因之间没有逻辑关联。
路径规划上我一般这么安排:在最外层建一个纯英文的根目录,下面按职责分层,SDK 一层、Android SDK 一层、工程代码一层、缓存一层。分层的好处是后面清理缓存、迁移机器、给同事拷环境的时候,不用在一堆系统目录里翻找。还有一个隐性收益:如果你哪天要把环境整体搬到另一台机器,直接压缩根目录拷过去,改改环境变量基本就能用。
解压之后不要急着跑命令,先确认两件事。一是解压是否完整,可以看目录下有没有bin、packages、bin/cache这几个关键目录;二是第一次运行flutter命令时会触发一次初始化下载,它会往bin/cache里拉取 Dart SDK 和若干工具产物,这一步网络不好会卡很久,属于正常现象,不要以为卡住了就中断,中断容易留下半截的缓存,后面反而得手动删。如果这一步反复失败,可以先把公共镜像源配好再重来,镜像配置我在第 5 节展开。
一个常见的误区是把 Flutter SDK 装到需要管理员权限的系统目录里。这样做的后果是:后续 SDK 升级、缓存写入、插件下载都可能因为权限不足失败,而在某些系统上这种失败表现得非常隐晦——不报“权限不足”,而是报下载超时或者文件写入异常。所以尽量放在当前用户可完全控制的目录下。
2.2 环境变量:Path 里到底该放几条
环境变量这一步,很多教程只写“把 flutter/bin 加到 Path”,但实际上一套能长期用的配置需要好几个变量,而且它们的作用各不相同,混在一起讲就容易记错。
| 变量 | 指向 | 作用 | 常见错误 |
|---|---|---|---|
| Path | Flutter SDK 下的 bin 目录 | 让终端能找到 flutter 命令 | 指到了 SDK 根目录而不是 bin |
| Path | Android SDK 下的 platform-tools | 让 adb 等命令可用 | 与 Flutter 的 bin 顺序颠倒导致命令冲突 |
| JAVA_HOME | JDK 安装根目录 | 构建时定位 Java 环境 | 指到了 JDK 的 bin 目录 |
| ANDROID_HOME | Android SDK 根目录 | 供构建工具和 IDE 定位 SDK | 与 IDE 内置路径不一致 |
| 镜像变量 | 公共镜像地址 | 加速依赖与产物下载 | 只配了一个,另一个没配导致部分卡住 |
这里面最容易错的是 JAVA_HOME。规则是:指向 JDK 的根目录,不是 bin 目录。判断方法很简单,如果你设的路径下直接有bin/java可执行文件,那就是对的;如果你设的路径下只有一个bin文件夹,那就多了一层。
第二个容易出错的是 Path 里的顺序。如果你的机器上同时装了多个 Java 版本或者多个 Flutter 版本,Path 里靠前的那个会生效。我见过因为旧版本残留在 Path 靠前位置,导致flutter --version显示的和实际目录里的版本对不上,排查半天以为是缓存问题。所以每次改完 Path,务必新开一个终端窗口,把flutter --version、java -version、adb version三条命令都跑一遍,确认输出和你预期一致。旧终端不会重新加载环境变量,这是个低级但高频的坑。
至于镜像变量,它们具体影响的是包索引查询和产物下载这两个环节。之所以要分开配,是因为这两类请求走的域名不同,只配一个的话,另一类请求仍然会走默认地址,表现出来就是“依赖能解析但下载很慢”或者“下载很快但依赖列表拉不出来”这种半好半坏的状态。判断有没有配全,办法是清一次缓存后重新拉取依赖,看过程是否都顺畅。
2.3 Android 侧:JDK、SDK、构建工具的三方关系
Android 这一侧是环境搭建的重灾区,因为它涉及三个互相牵制的组件:JDK、Android SDK、Gradle。搞不清它们的关系,就会陷入“升了这个坏那个”的循环。
先说 JDK。现在主流的做法是直接用 Android Studio 自带的那套 JDK,位于其安装目录下的jbr目录里。这样做的好处是版本天然匹配,不用另外装;坏处是如果你在终端里直接跑 Gradle 命令,需要手动把 JAVA_HOME 指过去。如果你想独立管理,那就单独装一个 LTS 版本的 JDK,并且记住:Flutter 新版本普遍要求较新的 JDK,老工程里的 Gradle 又可能不支持太新的 JDK。这是一对现实矛盾,处理办法就是多版本共存,用的时候切换 JAVA_HOME。
再说 Android SDK。通过 IDE 的 SDK 管理器安装时,至少要勾选这几类:平台版本(对应你要编译的目标版本)、构建工具、平台工具、以及模拟器相关组件。很多人只装了平台版本,结果flutter doctor提示缺构建工具或者缺 adb,就是因为漏勾了。另外,SDK 的存放路径同样要遵守纯英文无空格的规则,而且一旦选定就不要轻易改,因为改了之后需要同步更新 ANDROID_HOME 和 IDE 里的配置,工程里也可能有本地配置文件记着旧路径。
最后是 Gradle。这里有一个必须建立的认知:Gradle 版本和 Android 构建插件版本是强绑定的,不能各升各的。工程里通常有两个地方涉及版本,一个是 Gradle 自身的版本,一个是构建插件的版本,两者必须落在官方给出的兼容区间内。当你从别处拉来一个工程跑不起来,报的错又和构建脚本相关,第一反应应该是去看这两个版本号,而不是盲目升级到最新。
另外还有个常被忽视的点:Gradle 的守护进程会吃掉不少内存。默认配置下,同时开几个工程构建,机器就容易变得很卡。我一般会在工程的构建配置里显式设置内存上限,比如给到 4GB,同时开启并行构建和构建缓存。参数不是越大越好,超过物理内存会导致频繁换页,反而更慢。经验值是:机器 16GB 内存给 4GB,32GB 给 6GB,再往上收益不明显。
2.4 iOS 侧:命令行工具与原生依赖管理
如果你只做 Android,这节可以先跳过,但只要目标平台包含 iOS,就必须先把这套东西装齐,否则flutter doctor会一直挂一条警告,而且真机调试时会直接失败。
第一件事是装 Xcode 命令行工具。注意这里有个顺序:先完整安装 Xcode,再执行命令行工具的安装命令,最后同意许可协议。很多人卡在“许可协议未同意”这条报错上,原因就是装完 Xcode 后从来没跑过同意许可的那条命令。这条命令需要管理员权限,执行时会让你滚动阅读一长段文本,读到底输入同意即可。
第二件事是原生依赖管理工具。Flutter 的 iOS 工程在构建前会执行一次依赖安装,如果这个工具缺失或者版本太旧,构建就会在这一步中断。安装方式有几种,我倾向于用系统包管理器安装,避免和系统自带的运行时版本纠缠。装完之后要验证版本,因为版本过低会导致依赖解析失败,而报错信息往往指向具体的依赖包,看起来像是包的问题,实际是工具太旧。
第三件事是签名配置。这一步不属于“装环境”,但它是第一次真机运行必然撞上的墙。模拟器不需要签名,所以很多人模拟器跑得飞起,一插真机就懵。建议的做法是:环境搭完后立刻在 Xcode 里打开一次生成的工程,把开发者账号和签名团队配置好,同时确认设备已注册。这些操作在 IDE 里点几下就完成,但如果等到写完代码才发现,会很打断节奏。
还有一个实操细节:iOS 的构建缓存目录非常大,一个中等工程跑几次就能占到几个 GB。如果你机器空间紧张,定期清理这个目录是必要的,但清完之后第一次构建会明显变慢,这是正常代价,不要以为环境坏了。
2.5 IDE 与插件:编辑器选谁,插件装哪几个
编辑器这件事没有标准答案,但有两个现实约束值得说清楚。
如果你要维护原生工程里的配置(比如签名、权限声明、原生依赖),用功能更全的那套 IDE 会省很多事,因为它对原生工程的支持是原生的;如果你日常就是写 Dart、跑热重载、看日志,轻量的编辑器启动快、占用小,体验更顺。我自己是两套都装,重活开重的,日常改 UI 开轻的,工程目录共用,互不干扰。
插件方面,核心的只有两个:语言支持插件和运行调试插件,前者提供代码补全、跳转、格式化,后者提供设备选择、热重载按钮、调试面板。这两个装好,剩下的都是锦上添花。我见过有人装了十几个相关插件,结果互相抢快捷键、抢格式化规则,代码保存时被改成两套风格,反而添乱。
需要特别提醒的是格式化配置。Dart 的格式化规则会随 SDK 版本变化,如果编辑器里配置的行宽和团队约定不一致,提交代码后会出现大量纯格式的 diff,非常影响 code review。建议在工程根目录放一份分析配置文件,把行宽、规则集写进去,让编辑器读取工程配置而不是全局配置,这样团队里所有人格式化结果一致,省掉无数无意义的争论。
3. 动手实操:从零到第一次成功运行
3.1 落地顺序:先下游后上游
前面讲了原理,这里给一套可以直接照做的顺序。我把顺序设计成“从下游往上游”,理由是每一步都能独立验证,出错时定位范围小。
第一步,装 JDK。选一个 LTS 版本,装到纯英文路径,配置 JAVA_HOME,然后在终端验证java -version输出版本号且没有报错。这一步不通,后面全白搭。
第二步,装 Android SDK。通过 IDE 的 SDK 管理器安装,勾选平台、构建工具、平台工具、模拟器四类。装完在终端验证adb version能输出版本。如果提示命令不存在,说明 platform-tools 目录没进 Path,回去补。
第三步,装 Flutter SDK。下载压缩包解压到规划好的目录,把 bin 目录加进 Path,配好镜像变量,然后执行flutter --version。第一次会初始化下载,耐心等它走完,看到版本号和渠道信息就说明 SDK 本身没问题。
第四步,把 Flutter 和 Android 绑起来。执行 SDK 路径配置命令,把 Android SDK 的路径告诉 Flutter;然后执行许可接受命令,把一系列 SDK 许可一次性同意。这一步不做,构建时会报许可未接受,而这个报错在构建日志里位置很靠后,容易被忽略。
第五步,体检。执行flutter doctor -v,注意加-v,详细模式能看到具体路径和版本,比简版有用得多。然后按“缺什么补什么”的原则逐条处理,顺序是从最底层的缺失项往上修。
这套顺序我在不同机器上走过很多次,只要每步都验证过,最后一次性全绿的概率很高。反过来说,如果你跳步,比如跳过 JDK 验证直接装 Flutter,那么后面任何一个构建错误,你都得先怀疑是不是 JDK 的问题,排查成本翻倍。
3.2 逐条消红:flutter doctor的正确读法
flutter doctor的输出分几大块,每块前面有个状态标记。我的处理原则是:只看有问题的块,从上往下修,修完一块重跑一次。不要一次性修完所有再跑,因为后面的问题有可能是前面的问题导致的连锁反应。
第一块是 Flutter 自身。这里出问题一般是缓存损坏或者渠道异常,处理办法是清缓存后重新初始化。清缓存命令会删掉bin/cache下的产物,重新执行 flutter 命令时会自动重拉,网络通畅的话几分钟就恢复。
第二块是 Android 工具链。常见提示有三种:缺 SDK、缺许可、缺构建工具。缺 SDK 就去配路径,缺许可就跑接受命令,缺构建工具就去 SDK 管理器补装。
第三块是 IDE 插件。这里提示的是某个编辑器没装 Flutter 插件,不影响命令行构建,按需处理即可。
第四块是设备。提示“未连接设备”是最常见的,因为还没开模拟器。这时候不要急着建模拟器,先用命令列出可用设备,确认工具能识别到已经开着的模拟器或真机。
还有一块是网络资源检查,它探测的是几个关键下载地址是否可达。如果你的网络访问这些地址很慢,这里会一直挂着提示。它不影响本地构建,但因为每次doctor都会跑一遍,会拖慢整个体检过程。我一般会在确认其他项都正常之后,用跳过网络检查的参数跑一次,拿到干净的结果。
这里分享一个判断技巧:如果某一块的提示信息里带了具体路径,那就说明它找到了这个组件,只是在核对版本或者状态;如果提示里完全没提路径,那就是压根没找到。这两种情况的处理方式完全不同,前者要查版本兼容,后者要查环境变量和安装完整性。
3.3 创建工程与首次运行:把变量控制到最少
环境体检全绿之后,别急着在已有工程上试,那会引入太多变量。先用命令新建一个最小工程,跑通它,再回头处理你的目标工程。
新建命令里有两个参数建议显式指定:一个是组织标识,它会影响包名,后面上架或者接第三方服务都会用到,中途改很麻烦;另一个是工程名,注意只能用下划线命名,不能用横杠,这是 Dart 包命名的强制要求。工程名一旦定了,改起来涉及目录名、包名、配置文件多处,很烦。所以第一次建工程时多花三十秒想名字,后面省半小时。
建好之后先别急着跑,先确认目录结构。根目录下你会看到几个关键文件和目录:依赖声明文件、分析配置文件、以及各个平台的原生工程目录。理解这个结构的意义在于,你会知道哪些代码是共享的、哪些是按平台定制的。共享的那部分是 Dart 代码,定制的那些是各平台的原生壳工程——这也是 Flutter 项目在需要深度定制原生能力时依然能下手的入口。
运行分两步:先用设备列表命令确认目标设备在线,再用运行命令启动。第一次启动会做完整的构建,耗时明显长于后续,因为要下载依赖、编译原生壳工程、生成本地产物。这个过程如果卡住,看日志里的最后一行是哪个任务,基本就能定位到卡在依赖下载还是编译阶段。
首次运行成功后,别关掉,故意改一行界面代码保存,看热重载是否生效。这个验证很关键,因为热重载依赖调试连接,连接有问题时虽然能启动但热重载会失效,而你会在写了两小时代码之后才发现每次改都要重启,效率损失巨大。
3.4 写一个自己的启动脚本:把每次要敲的命令收起来
环境搭好后,日常开发其实就那么几个动作:选设备、拉依赖、运行、清缓存。但它们都带一长串参数,敲多了烦,记错一个参数就白等。我的习惯是在工作目录下放几个脚本,把固定参数固化进去。
比如一个“干净启动”的脚本,先拉依赖,再跑构建,遇到问题就清缓存重来;再比如一个“切换目标设备”的脚本,把设备标识作为参数传进去。脚本不用写得多优雅,能省掉记忆负担就行。
这里有个心得:脚本里的路径一律用变量,不要写死绝对路径。写死的后果是换机器、换目录、给同事用的时候全部失效,还得逐行改。用变量的成本是多写一行定义,收益是可移植。我一般会把 SDK 路径、工程路径、设备标识都提成变量放在脚本顶部,一眼能看全。
再补一个细节:脚本执行失败时,默认可能继续往下跑,导致后面的步骤在错误状态下执行,产生一堆误导性日志。稳妥的做法是在脚本开头加上“遇错即停”的设置,让它在第一个失败点就中断。这样日志干净,定位也快。
4. 踩坑实录:环境问题排查速查表
4.1 先分类再动手:三类问题的判断方法
环境出问题时,最忌讳的是“看到报错就搜”。我一般先花一分钟做分类,因为不同类别的排查路径完全不同。
第一类是找到不到工具,典型表现是命令不存在、路径找不到、文件缺失。这类问题九成出在环境变量上:变量没配、配错层级、或者配了但终端没重新加载。处理办法是回终端验证每一条路径变量的实际值,然后手动进那个目录看文件在不在。
第二类是能跑但构建失败,典型表现是依赖解析失败、版本冲突、编译报错。这类问题看报错的第一行比最后一行有用,因为构建工具会把根本原因放在前面,后面全是级联的失败信息。判断的关键词是版本号和不兼容,看到就直奔兼容矩阵去查。
第三类是能构建但运行异常,典型表现是装到设备上闪退、白屏、功能不响应。这类问题往往不是环境问题,而是配置或代码问题,比如权限没声明、原生依赖没注册。这类排查要看设备日志而不是构建日志,两者的信息完全不同源。
分类之后,还有个技巧:改一个变量就重跑一次,不要一次改五处。我见过有人对着报错连改五处配置,然后问题消失了,但他不知道是哪一处起了作用,下次遇到同样问题还得重来一遍。环境配置这件事,可复现比“能跑”更值钱。
4.2 高频问题速查表
下面这张表是我这些年攒下来的,基本覆盖了新人搭建环境时八成的卡点。
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| 命令提示不存在 | 路径没进环境变量,或终端未重载 | 核对变量值,新开终端验证 |
| 首次初始化卡住很久 | 正在下载工具产物,或网络受阻 | 观察缓存目录是否在增长,必要时配镜像 |
| 构建报许可未接受 | 未执行许可接受命令 | 补跑许可命令,同意全部条目 |
| 构建报某版本不兼容 | 构建工具与插件版本不匹配 | 查兼容矩阵,成对调整版本 |
| 依赖拉取失败 | 包索引地址不可达或缓存损坏 | 清缓存重试,检查镜像变量是否配全 |
| 模拟器起不来 | 硬件虚拟化未开启,或镜像未下载 | 进固件设置开启虚拟化,补下系统镜像 |
| 真机连不上 | 调试模式未开,或驱动/授权未完成 | 开调试模式,重新插拔并确认授权弹窗 |
| 构建成功但装不上 | 设备架构与产物不匹配 | 检查目标架构配置 |
| 热重载无效 | 调试连接断开 | 重启调试会话,检查端口占用 |
| 构建极慢 | 首次构建无缓存,或内存配置过低 | 调高构建内存,确认缓存开关已开 |
表里每一行背后都有具体场景,我挑两个最费时的展开说。
“构建报某版本不兼容”这一条,新人最常见的错误反映是“那我把它升到最新”。这个动作在环境没问题时是优化,在有问题时是加变量。正确做法是先看清楚是谁和谁不兼容:是构建工具和插件不兼容,还是 JDK 和构建工具不兼容。这两对关系的调整方向相反——前者要成对升或成对降,后者一般是降 JDK 而不是升构建工具。搞错方向,就会陷入“升完这个又报那个”的循环。
“模拟器起不来”这一条,很多时候不是软件问题。模拟器需要硬件虚拟化支持,而这个开关在机器出厂时可能是关闭的,需要进固件设置里打开。打开之后还要注意,某些系统上多个虚拟化方案会互相抢占,装了两个模拟器方案时可能出现“明明开了虚拟化却报不可用”,处理办法是确认只启用一套方案。这个坑我在三台不同机器上遇到过,每次都要重新回忆一遍排查顺序。
4.3 几条压箱底的处理技巧
第一条:遇到说不清的构建错误,先做一次彻底清理再重来。清理包括工程内的构建产物、依赖缓存、以及全局构建缓存。很多人只清了工程内的产物,全局缓存里的脏数据还在,重跑之后错误照旧,于是判定“清理无效”。清理完第一次构建会慢,这是必然的。
第二条:日志不要只看末尾。构建工具的日志顺序是先报根本原因,后面是一连串的衍生失败。末尾那些“任务执行失败”基本都是噪音。看日志的正确姿势是从上往下找第一个带有具体原因的行。
第三条:把环境信息记录成文档。这不是形式主义。我在每个项目的仓库里放一份环境说明,写清楚 SDK 版本、JDK 版本、构建工具版本、以及关键配置项。新人进来照着装,出了问题一比对就知道差在哪。这份文档的维护成本极低,收益极高,尤其是在团队里有人换机器或者加人的时候。
第四条:别在环境有问题时写业务代码。听起来像废话,但实际很常见:热重载失效了,就手动重启,重启要等一分钟,一天下来浪费的时间非常可观,而且打断思路。环境问题应该在十分钟内解决掉,解决不了就先停下来专门处理,不要带着问题开发。
第五条:备份一份能用环境。配置全部验证通过后,把 SDK 目录、环境变量配置、关键配置文件打包存一份。下次换机器或者环境被搞坏时,恢复比重装快得多。这一步我是在吃过一次亏之后才养成习惯的:某次升级后环境彻底崩掉,重装加调试花了大半天,而如果当时有个备份,恢复也就十几分钟。
5. 环境通了之后:让它长期好用的几个配置
5.1 镜像与缓存:把重复下载的成本降下来
环境能跑只是起点,真正影响日常体验的是依赖下载速度。Flutter 的依赖分两类:Dart 生态的包和原生侧的构建依赖,它们走的是不同的仓库,需要分别配置。
Dart 侧的镜像通过环境变量配置,一般需要配两个:一个指向包索引服务,一个指向产物存储服务。两个都要配,只配一个会出现半好半坏的状态,前面提过。配置完成后建议清一次缓存再拉依赖,确保新配置真正生效,而不是命中旧缓存让你误以为配好了。
原生侧的依赖走的是另一套仓库机制,需要在工程的构建脚本里声明仓库地址。这里的原则是:把公共镜像地址放在仓库列表靠前的位置,官方地址保留在后面兜底。这样既享受了速度,又不会因为镜像缺包而彻底失败。需要注意,仓库声明的位置在不同版本的构建脚本里有变化,新版倾向于集中在一个统一的地方声明,老版则分散在各个工程文件里。改之前先看清楚当前工程的脚本结构,别照着老教程改错文件。
缓存这一块,我建议定期清理但不要频繁清理。清理的收益是释放空间、消除脏数据导致的神秘错误;成本是下次构建变慢。合理的节奏是:遇到说不清的构建错误时清一次,以及每隔一段时间集中清一次,其余时候别动。
5.2 多版本共存:把切版本变成一条命令
前面提过要一个版本一个目录,这里说具体怎么用。
最朴素的做法是维护几套环境变量,切版本时改 Path 的顺序。缺点是要手动改,容易忘。稍微好一点的做法是写个切换脚本,把不同版本的路径写成配置,执行脚本时自动改当前会话的环境变量。这个方案的优点是零依赖,缺点是只对当前终端会话生效,IDE 里还得单独配。
再进一步是用版本管理工具,这类工具的原理其实很简单:它把多个 SDK 版本放在统一目录下,然后在项目里放一个版本声明文件,进入项目目录时自动切换。好处是切换粒度精确到项目,坏处是多了一个需要维护的工具,以及它对某些 IDE 的自动识别支持不一定完善。我用下来的建议是:如果你同时维护三个以上不同 Flutter 版本的项目,值得上工具;如果只有一两个,脚本加目录管理就够了,别为了工具而工具。
不管用哪种方案,有一条必须遵守:切换版本后要清一次构建缓存。不同版本的构建产物格式可能不兼容,混在一起用会出现很难解释的错误,比如编译通过但运行白屏。清缓存这一步花几分钟,能省掉几小时的排查。
5.3 新工程默认配置:一开始就设对,后面少改
最后说说新工程的初始化配置,这些设置改起来不难,但改晚了要动很多文件。
第一项是依赖版本约束的写法。尽量用范围约束而不是固定版本,范围能让你在拉依赖时拿到兼容的补丁版本,固定版本则会让整个依赖树僵化,后面想升某个包时会发现处处受限。当然范围也不能太宽,太宽容易拉到不兼容的大版本,一般是锁定主版本号,放开次版本和补丁版本。
第二项是分析规则配置。把团队约定的规则写进工程内的配置文件,包括行宽、禁用哪些写法、开启哪些检查。这份配置会和编辑器联动,让所有人保存时格式一致。这件事越早做越好,等到代码写了几万行再来统一格式,diff 会非常难看。
第三项是构建产物的忽略配置。把构建产物目录、本地配置文件、缓存目录都加进忽略列表,避免误提交。这块最容易漏的是本地环境配置文件,一旦提交上去,别人拉下来会拿到你的本地路径,构建直接失败。
第四项是目录结构的约定。Flutter 工程默认只有一层源码目录,项目稍大就会乱。建议在开始写业务代码之前就定好分层,比如按功能分目录、把共享组件单独放一层、把平台相关代码隔离开。这个决定不用一开始就很完美,但要有,因为后面迁移成本会随时间线性上升。
我个人在实际操作中的体会是,环境搭建这件事的价值被严重低估了。大部分人把它当成一次性的体力活,装完就忘,结果在之后的每一周都要为当初的随意付出一点代价:构建慢一点、切版本烦一点、出错排查久一点。这些代价单次都不大,累积起来很可观。把两三个小时花在把环境理清楚上,后面几个月都会顺很多。尤其是路径规范和版本管理这两件事,前者决定你会不会遇到那些毫无道理的报错,后者决定你半年后接老项目时是十分钟上手还是折腾一天。