Flutter 踩坑流水账

Flutter 作为前端一个发展方向大热,有必要了解一下。

踩坑流水账,遇到啥记啥,不求全;遇到的 弯弯绕绕,错误示范如实记载 。当前 Flutter 还不太稳定,可能过一段时间操作细节就变了(希望是改进),这种记录可以跟将来做对比。

我的水平远远够不上厉害,但在一些朋友看来可能有可取之处,当他们自学遇到困难时,偶尔会问我是怎么学的。记录这样的流水账,你可以看到,我遇到新事物,一样是一点点试错。相信真正的大牛,(至少曾经)也是这样过来。 只不过大牛可能悟性好,试错时间和次数少一些。自己试错多了,也会掌握试错的套路。

某个程序员朋友,连 help 和 man 都不知道,让人着急。可我高中时,明明已经学过简单的编程,对 Linux 居然连听都没听过,听同学讨论一头问号。我的反应是什么?放假第一时间让我的电脑变成 Windows / Ubuntu 双系统。他之前不知道 help,现在知道了。互联网时代,知道了关键词,就不能说你一无所知。起码要把公开资料看完吧?

你看到的『最佳实践』的文章,其实已经是作者熟练之后提炼的结果。好处是读者不必再走一次弯路,坏处是读者可能会产生心理落差。

—— 这段文字跟本文内容关系不大,发散一下,算是呼应之前《S/L 大法——平凡人的做事方法》的观点吧。

后面如果觉得 Flutter 开发(在当前、对我而言)可以入坑,再来写经过整理总结的经验帖,否则就此一篇。

环境配置

获取 Flutter stable

考虑 flutter 还在快速开发中,为了方便更新,直接拉仓库的 stable 分支。

1
git clone -b stable https://github.com/flutter/flutter.git

得到的版本是 1.12.13+Hotfix.8

然后运行 flutter_console.bat ,按提示,先执行

1
flutter doctor

提示 powershell 版本太低,这时才想起,去看看依赖(这台老电脑买的时候预装 Win8,因为 Win8 只是个过渡产物很快升了 Win10。结果某个硬件驱动不兼容 Win10,干脆退回 Win7。)

  • Windows PowerShell 5.0 或者更高的版本(Windows 10 中已经预装了)

  • Git for Windows 2.x,并且勾选从 Windows 命令提示符使用 Git 选项。

    如果 Windows 版的 Git 已经安装过了,那么请确保能从命令提示符或者 PowerShell 中直接执行 git 命令。

Powershell

git 作为最高频的工具,几乎是最新的。而平时不用的 Powershell,什么版本真的没概念,赶紧打开一个 Powershell,输入

1
$PSVersionTable

才 2.0,装个最新的吧。

installing-powershell

发现 6.0 以上不能直接安装,还得安装 Powershell Core (因为从 Win 专用变成跨平台了)。要不要装最新的呢?先不管,反正 .NET 4.5 以上是一定需要的,先看 .NET 的版本

1
2
3
4
5
6
7
8
9
10
11
$DotNetVersions = Get-ChildItem HKLM:\SOFTWARE\WOW6432Node\Microsoft\Updates | Where-Object {$_.name -like
"*.NET Framework*"}
ForEach($Version in $DotNetVersions){
$Updates = Get-ChildItem $Version.PSPath
$Version.PSChildName
ForEach ($Update in $Updates){
$Update.PSChildName
}
}

是 4.7 ,够新了。

WMF

然后是 WMF(Windows Management Framework)。

wmf-install-configure

不同版本的 Powershell 和 WMF 的依赖关系如下:

Powershell 版本 PS 5.0 PS 5.1 PS 6.0+
依赖的 WMF 版本 WMF 5.0 (已包含 PS 5.0) WMF 5.1 (已包含 PS 5.1) WMF 4.0+

结论,不管我装不装 6.0+,安装一个 WMF 5.1 总是没错的。

下载,解压,按说明在 Powershell 里运行安装的 ps1 脚本。提示没有签名。

1
2
get-executionpolicy -list
set-executionpolicy remotesigned -scope process

再运行一次,可以了。

这时再看 Powershell 的版本。

1
2
3
4
5
$PSVersionTable.PSVersion
Major Minor Build Revision
----- ----- ----- --------
5 1 14409 1005

Powershell 5.1 已经包含在 WMF 5.1 了,已经满足要求,就没啥动力继续折腾 6.0+ 了。

Flutter 镜像

回到 flutter_console ,继续 flutter doctor ,这回 Powershell 不报错,开始自动下载 Dart SDK 了(难怪没有要求先安装 Dart):

1
2
Checking Dart SDK version...
Downloading Dart SDK from Flutter engine e1e6ced81d029258d449bdec2ba3cddca9c2ca0c...

貌似因为网络不好,下载过程重试了几遍,还好最终成功了

1
2
3
4
5
6
Checking Dart SDK version...
Downloading Dart SDK from Flutter engine e1e6ced81d029258d449bdec2ba3cddca9c2ca0c...
Unzipping Dart SDK...
Building flutter tool...
Running pub upgrade...
Error (69): Unable to 'pub upgrade' flutter tool. Retrying in five seconds... (9 tries left)

同样的,pub upgrade 也重试了很多遍,不过次数太多,引起注意,先停掉。翻到

在中国网络环境下使用 Flutter

扫了一眼,直接用 Flutter 社区的镜像。因为是 Windows,打开环境变量,加上这两个值:

1
2
PUB_HOSTED_URL=https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

Android SDK

重新打开 flutter_console.bat,再次 flutter doctor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, v1.12.13+hotfix.8, on Microsoft Windows [Version 6.1.7601], locale
zh-CN)
[X] Android toolchain - develop for Android devices
X Unable to locate Android SDK.
Install Android Studio from: https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK components.
(or visit https://flutter.dev/setup/#android-setup for detailed instructions).
If the Android SDK has been installed to a custom location, set ANDROID_HOME to that location.
You may also want to add it to your PATH environment variable.
[!] Android Studio (version 3.6)
X Flutter plugin not installed; this adds Flutter specific functionality.
X Dart plugin not installed; this adds Dart specific functionality.
[!] Connected device
! No devices available

作为一个前 Android 开发者,电脑上其实一直有 Android Studio 和 Android SDK。

开始这篇流水账之前,我已经把 Android Studio 升级到当下最新的 3.6,SDK tools 也升级到最新的 26.1.1。只是 Android Studio 能定位 SDK 的位置,所以没有设置 ANDROID_HOME。

因为 Android Studio 已经很稳定,下载很快,安装很傻瓜,就不提了。

SDK version

新添加 ANDROID_HOME ,重开 console(以重新加载环境变量),再次 flutter doctor ,SDK 居然不是最新,而且也不是 Android Studio 里显示的 26.1.1,可能升级时漏了什么:

1
2
3
4
5
6
7
8
9
[!] Android toolchain - develop for Android devices (Android SDK version 27.0.3)
X Flutter requires Android SDK 28 and the Android BuildTools 28.0.3
To update using sdkmanager, run:
"%ANDROID_HOME%\tools\bin\sdkmanager" "platforms;android-28" "build-tools;28.0.3"
or visit https://flutter.dev/setup/#android-setup for detailed instructions.
X Android license status unknown.
Try re-installing or updating your Android SDK Manager.
See https://developer.android.com/studio/#downloads or visit
https://flutter.dev/setup/#android-setup for detailed instructions.

那就去跑一下 sdkmanager 吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sdkmanager.bat
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81)
at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:73)
at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:48)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190
)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499)
... 5 more
# 改为写 go 之后好久没写 Java,难道是版本太旧?
java --version
java 10.0.1 2018-04-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)

虽然因为有一段时间没写 Java,并非最新的 LTS 版本(当前是 Java8 u241 或者 Java 11.0.6),但 Java 10 也不旧。我还特意看了 Java 10 的文档,确定有 javax/xml/bind/annotation/XmlSchema 。而且 sdkmanager 的版本也很迷。

(后来发现,Java 很可能是太新,而不是太旧,所以需要指定 JavaEE 包。据说 Java 11 已经移除了 JavaEE (变成了雅加达独立出去了),更加运行不了。)

暂时没有兴趣深入了解哪里出了问题,直接卸载 Android SDK tools,然后下载最新的 SDK tools 压缩包。

https://www.androiddevtools.cn/ 有收集。注意只是收集各个版本的地址而已,来源还是官方的,下完可以校验一下。这回的 SDK tools 在 Android Studio 显示 26.0.1,终于可以调用了。有一个可用更新 26.1.1,感觉这里就是问题所在,又给升了级,果然又出现上面的 NoClassDefFoundError

先不管这个问题,退回到可用的 26.0.1。这是重新认真看 Doctor summaries,发现 "platforms;android-28" "build-tools;28.0.3" ,也就是它要求的所谓 SDK Version,并非我理解的 SDK tools 的 version,而是分别指 platform 和 build-tools 的版本,对应 Android 9(Pie)。为什么迟迟没有发现这点?因为在flutter 官方的引导上面有这么一句话

To prepare to run and test your Flutter app on an Android device, you’ll need an Android device running Android 4.1 (API level 16) or higher.

被我理解为了开发的 API level 是 16+。

license status unknown

再次 flutter doctor,这回 SDK 版本不报错了,只剩下 Android license status unknown.

1
2
3
flutter doctor --android-licenses
A newer version of the Android SDK is required. To update, run:
%ANDROID_HOME%\tools\bin\sdkmanager --update

跟上面说的 Try re-installing or updating your Android SDK Manager. 一致。

1
sdkmanager --update

NoClassDefFoundError 又出现了…… 本来不想在这上面耗时间,看来是躲不过了。

最后找到StackOverflow 的两个问题的回答: StackOverflow 问题1StackOverflow 问题2

编辑器打开 sdkmanager.bat,找到 DEFAULT_JVM_OPTS,在后面追加 -XX:+IgnoreUnrecognizedVMOptions --add-modules java.se.ee ,注意 Windows 环境不需要额外在外面加单引号,加了会导致变量扩充 %~dp0\.. 出错,加完之后那一行变成了:

1
set DEFAULT_JVM_OPTS="-Dcom.android.sdklib.toolsdir=%~dp0\.." -XX:+IgnoreUnrecognizedVMOptions --add-modules java.se.ee

然后再次执行

1
2
sdkmanager --update
done

没有任何变化,版本没变,文件修改时间没变,然后 flutter doctor --android-licenses 依然说需要新版本。

然后尝试执行

1
sdkmanager --licenses

提示有 licenses 还没 accept,赶紧一路 yes。

这时我试着再 flutter doctor 一次,居然通过了。说明版本旧不是根本问题。关键是 accept 掉 licenses

升级 SDK tools

本来这样就完事了,但还是有点好奇。记不记得 Android Studio 可以直接升级 SDK tools 到 26.1.1 ,那就升级看看。

Android Studio 确实可以升级 SDK tools,但是执行到最后,升级失败。因为升级靠的是打补丁,升级程序检查到 sdkmanage.bat 有过修改,补丁失败。然后升级程序自动改为全量安装,不知道是不是因为打开了某些目录或者文件没管,继续失败。我猜 sdkmanage --update 也是因为类似的原因所以失败了,只是没有报错信息。

这时我想到两个选择:

  1. 把 sdkmanager.bat 改回去,然后用 Android Studio 升级。
  2. 把修改之后的 sdkmanager.bat 改个名字,然后复制一份原名的改回去。然后执行修改过的 bat。

(因为没有备份)这需要一字不差改回去,还好改动不多。我尝试了 1 ,成功了。2 就没有验证。

升级完 还得把参数加回去 ,不然又会出现 NoClassDefFoundError

继续 flutter doctor ,这回

1
2
[!] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

这次有了经验,我没有按它的来,而是 sdkmanager --licenses 。应该是升级过的 tools 还要再确认一遍 licenses。

Android Studio Plugins

Settings > Plugins 或者直接在打开项目的界面 Configure > Plugins, 直接搜索 Flutter 安装,安装过程中会提示依赖 Dart 一起安装。

如果中间出现网络不好导致超时,就不要同时装,先装 Dart,再装 Flutter。

据说用 VSCode 开发也可以,我也有在用 VSCode。但是既然 flutter doctor 要求 Android Studio,就先按它的套路走通。

真机 or AVD

最后一步就比较简单了,要么插个真机,要么创建一个 AVD。因为没有多余手机,随便选了一个 Pixel 3 + 奥利奥8.0 x86_64 的AVD。

1
2
3
4
5
6
7
8
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, v1.12.13+hotfix.8, on Microsoft Windows [Version 6.1.7601], locale
zh-CN)
[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[√] Android Studio (version 3.6)
[√] Connected device (1 available)
• No issues found!

这四个 ✔ 得来不易。

第一个项目

安装 flutter 插件之后的 Android Studio ,New 菜单会多出一个 New Flutter Project,有引导,没啥值得说的,一路 Next。结果创建完之后,在生成项目的界面转了半天。第一次生成,对需要多长时间没有概念,以为是老机器卡,就先丢下去干别的。等我离开电脑半天,回来还在转,事情就不太正常了。

flutter lockfile

试图强关 AS 无效之后,只好直接杀掉进程。这时去看 AS 的 workspace,项目看起来已经生成好了。试着重新打开 AS,可能是因为杀进程丢失了一些配置,需要重新指定 Android SDK 的位置。打开 AS 没看到新建的项目,就去打开前面生成的项目。

打开成功,没有提示缺这少那,说明项目生成是没什么问题的。但是初始化之后(自动生成的)代码一片红色,明显是缺少依赖引起的,同时 AS 也弹出提醒,还没有run flutter packages get ,旁边给了几个选项,分别是 Get dependencies 和 Update dependencies(第三个忘了)。

一次都还没获取依赖,自然点第一个,底下的 log 显示:

1
2
3
flutter.bat --no-color packages get
Running "flutter pub get" in myapp...
Waiting for another flutter command to release the startup lock...

查了一下,flutter 有一个全局的 lockfile,在 flutter/bin/cache/lockfile ,每次只允许一个进程打开,作为全局锁。这种套路经常跟 Linux 打交道的应该很熟悉。按网上的说法,把它删掉(因为有进程占用,使用了 unlocker 释放句柄),然后再来。

flutter pub get

这回只是输出少了一行,然后又陷入了无尽的等待。

1
2
flutter.bat --no-color packages get
Running "flutter pub get" in myapp...

没办法,退出 AS 再来(这次没有生成的对话框,可以正常退出),然后又提示锁的问题。死循环了。

锁的问题前面已经说了,是因为 dart 执行命令异常退出,没有释放锁,强制释放就行(后来发现 lockfile 不用删,释放掉就行。当然如果没有 unlocker 之类的工具,强删也行,反正就一个大小为 0 的文件,没有会自动新建)。问题在于为什么第一次会卡死。

pub 是 dart 的包管理器,如同 pip 于 Python,go mod 于 go,Maven 于 Java,npm 于 nodejs… 试了一下,pub 不能直接调用,大概因为没有独立安装 dart,而 flutter pub 是针对 flutter 的封装。

既然是获取依赖包这步卡了,那么大概率就是 GFW 的问题。还记得上面设置的镜像吗?这回释放完 lockfile,改在 flutter_console 里执行:

1
2
flutter packages get
Running "flutter pub get" in three_five_two... 5.9s

不到 6 秒完事,还真就好了。代码里的各种错误也没了。

Android Studio 的环境变量

可是每次都要手动执行命令也很烦。而且就算我愿意,AS 也不会每次执行命令停下来,告诉我对应的命令让执行;它一旦卡死,还得费劲去释放 lockfile。所以,是 AS 读不到设置的环境变量吗?

首先,flutter help 输出的指令里没有 packages ,然后 flutter help packages 告诉我们,packages 只是 pub 的别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
flutter help packages
Commands for managing Flutter packages.
Usage: flutter pub <subcommand> [arguments]
-h, --help Print this usage information.
Available subcommands:
cache Work with the Pub system cache.
deps Print package dependencies.
downgrade Downgrade packages in a Flutter project.
get Get packages in a Flutter project.
global Work with Pub global packages.
pub Pass the remaining arguments to Dart's "pub" tool.
publish Publish the current package to pub.dev
run Run an executable from a package.
test Run the "test" package.
upgrade Upgrade packages in a Flutter project.
uploader Manage uploaders for a package on pub.dev.
version Print Pub version.
Run "flutter help" to see global options.

flutter help packages get 一下。三层命令 help 进来,都没有看到 --no-color 参数干嘛用,先不管它。

这次模仿 AS 的调用方式,不直接在 flutter_console 操作,而是直接调用 flutter/bin/flutter.bat

1
2
3
4
5
6
7
8
flutter\bin\flutter.bat packages get
Running "flutter pub get" in three_five_two... 0.6s
flutter\bin\flutter.bat packages get
Running "flutter pub get" in three_five_two... 0.5s
flutter\bin\flutter.bat --no-color packages get
Running "flutter pub get" in three_five_two... 0.5s

完全没问题。其实没问题是应该的,因为打开 flutter_console.bat 看就知道,里面只做了两件事,把 flutter/bin/ 临时加入 PATH,然后打开一个 cmd,没了。

(好吧,其实是三件,前面还显示了 flutter 的 ascii-art 和 简单的说明。)

1
2
3
4
REM "%~dp0" is the directory of this file including trailing backslash
SET PATH=%~dp0bin;%PATH%
CALL cmd /K "@echo off & cd %USERPROFILE% & echo on"

会不会是因为依赖已经下载好了,根本没有触发网络访问?在 AS 再试一次,又卡了……(又要找 unlocker)

在 AS 里打开Terminal,从输出看其实就是一个 cmd,在这里再执行一次,又卡死了……

1
2
3
# 这里是 AS 里面的 Terminal
echo %PUB_HOSTED_URL%
%PUB_HOSTED_URL%

环境变量实锤了。

在对着 AS 的 Terminal 一顿 echo 之后,发现绝大多数环境变量都能读到,无论是系统变量,还是用户变量。什么 PATHGOROOTGOPATH….. 都有值,唯独 ANDROID_HOME 和 两个镜像地址。然后想了想,这几个值好像都是这次新建的,并且中间没有重启过。呃,人家 cmd 都是关掉重开就可以加载到,难道就你 AS 非要重启计算机?(AS 都重启好多遍了)

试试吧。重启,第一时间打开 AS。

1
2
3
4
5
echo %PUB_HOSTED_URL%
https://pub.flutter-io.cn
flutter\bin\flutter.bat --no-color packages get
Running "flutter pub get" in three_five_two... 2.9s
Process finished with exit code 0

我x [口吐芬芳],还真是!浪费时间。估计新建项目时也是卡在这里。有空研究一下 AS 的环境变量究竟是怎么加载的。这老电脑开关机慢,一堆文件也懒得关了重新打开,平时都是休眠的,改点啥都要重启真是为难人。

Run

接下来还剩一步,就是验证自带的模板程序能不能跑起来。

打开 AVD,启动前面创建的虚拟设备,然后选这个设备为 run target,然后 run。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Launching lib\main.dart on Android SDK built for x86 64 in debug mode...
Running Gradle task 'assembleDebug'...
# 再等一下就能结束战斗了
# 啪,打脸
FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring root project 'android'.
> Could not resolve all artifacts for configuration ':classpath'.
> Could not download kotlin-gradle-plugin.jar (org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50)
> Could not get resource 'https://jcenter.bintray.com/org/jetbrains/kotlin/kotlin-gradle-plugin/1.3.50/kotlin-gradle-plugin-1.3.50.jar'.
> Connection reset
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 8m 41s
Finished with error: Gradle task assembleDebug failed with exit code 1

这明显是 jcenter 的网络问题。侥幸认为应该只是慢,重试一遍,居然真的跑起来了。

下次还是换个 aliyun 的仓库镜像吧。

1
2
maven{ url'http://maven.aliyun.com/nexus/content/groups/public/' }
maven{ url'http://maven.aliyun.com/nexus/content/repositories/jcenter'}

第一次的编译时间特别久,老机器可能加重了这个情况。后续的修改用 hot reload 应该会快很多。


到这,一行代码都没写,但写代码前的准备工作,算是告一段落。不知不觉居然记了这么长流水账,前后断断续续折腾了两天多。时间大部分是被地理位置下访问技术资源的可达性及网速(简称 Qiang)拖累的,少部分是因为 flutter 有些小坑且文档有误导性。

在等网络和命令执行的间隙,快速扫了一眼 dart 的语法,从不同的地方看到了 Java、Python 还有 Go 的影子,当然也有一些比较原创的语法糖(如级联调用)。刚好这三门语言都还算熟,希望写起来不费劲。

这篇太长了,就此打住。


知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议进行许可。
本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。
请点击查看协议的中文摘要。