最近有需要在一个Flutter项目上继续开发,但我是第一次使用Flutter/Dart,这个项目最近一年也没有继续维护,所以在接手的过程中碰上不少有意思/烦人的事情。虽然没什么特别的技术含量,但还是记录一下,方便有需要的人参考。
简况
要接手的是一个研究项目的代码,由于没有开源,所以我就不放链接了。
该项目开发于2023年,之后没有继续维护,而且也没有留下说明文档。我又从来没有用过Flutter/Dart,所以接手过程也是同时学习工具链的过程。
我知道该项目是为了Android开发的,但不知道是否也在其他平台进行调试。接手过程中发现还用了Google的Firebase,于是也涉及了它相关的工具及问题。
在最初走了一点弯路以后,我使用了fvm来管理Flutter和Dart版本,还是比较顺利的——直接用fvm use选择合适的版本即可,会自动下载Flutter和对应的Dart版本。我通过时间进行推测,从比较早的Flutter版本(2023年的3.7.11
)开始尝试,逐步迁移到更新不少的版本。中间很多都可以自动解决,尤其是通过dart fix
可以解决由于Flutter和Dart版本变化而带来的一些改变(比如库中的参数名,变量类型),这还是挺好的(只是我个人始终有点对魔法的怀疑)。少部分时候在涉及编译到特定平台时,由于版本变化,需要手动调整配置,这个过程我也算熟悉了一下Flutter项目的构成:
-
项目的核心代码存在
lib/
目录下; -
项目下还有一些平台相关的脚手架代码,位于各个平台名称的目录下(比如
linux/
);-
这些代码普遍是自动生成的,如果没有需要的话完全可以不管它们;
-
随着Flutter版本更新,这些脚手架代码有时候也会变化,如果实在变化太大,可以删掉这些目录然后执行
flutter create --platforms=linux .
这样的命令来重新生成; -
如果进行了一些修改(比如我接手的项目在android上引入了额外依赖,也就是firebase),那么可能需要手动进行迁移了,不过官方文档写得还可以,虽然不完美但足够参考。
-
然而配置和试用下来有一些细节地方处理得还是不太好,有点糟心,报错也奇怪,解法也不见得好找;有时还有特定版本或特定平台的问题。
下面各节按问题呈现,没什么具体的理论讨论,只是呈现问题以及相关解决方法,并偶尔会有一些我的感受评价。
MacOS
由于工作机是MBP(参见《Linux用户的Mac初体验(及吐槽)》),最开始就在MacOS上进行配置,但发现MacOS及系统上的工具链本身会造成一些困难,也是挺难评的。
损坏的Dart SDK包
在MacOS上使用fvm安装工具链时,在下载Dart SDK的时候会出错(会输出到console),但脚本没有检测这个错误,而是继续执行,并且报成功。然而之后执行的时候会再次检测到没有安装成功,重复该步骤。具体输出如下:
Setting up Flutter SDK: 3.7.11
Downloading Darwin arm64 Dart SDK from Flutter engine 1a65d409c7a1438a34d21b60bf30a6fd5db59314...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 217M 100 217M 0 0 10.2M 0 0:00:21 0:00:21 --:--:-- 10.6M
UnZip 6.00 of 20 April 2009, by Info-ZIP. Maintained by C. Spieler. Send
bug reports using http://www.info-zip.org/zip-bug.html; see README for details.
Usage: unzip [-Z] [-opts[modifiers]] file[.zip] [list] [-x xlist] [-d exdir]
Default action is to extract files in list, except those in xlist, to exdir;
file[.zip] may be a wildcard. -Z => ZipInfo mode ("unzip -Z" for usage).
-p extract files to pipe, no messages -l list files (short format)
-f freshen existing files, create none -t test compressed archive data
-u update files, create if necessary -z display archive comment only
-v list verbosely/show version info -T timestamp archive to latest
-x exclude files that follow (in xlist) -d extract files into exdir
modifiers:
-n never overwrite existing files -q quiet mode (-qq => quieter)
-o overwrite files WITHOUT prompting -a auto-convert any text files
-j junk paths (do not make directories) -aa treat ALL files as text
-C match filenames case-insensitively -L make (some) names lowercase
-X restore UID/GID info -V retain VMS version numbers
-K keep setuid/setgid/tacky permissions -M pipe through "more" pager
See "unzip -hh" or unzip.txt for more help. Examples:
unzip data1 -x joe => extract all files except joe from zipfile data1.zip
unzip -p foo | more => send contents of foo.zip via pipe into program more
unzip -fo foo ReadMe => quietly replace existing ReadMe if archive file newer
It appears that the downloaded file is corrupt; please try again.
If this problem persists, please report the problem at:
https://github.com/flutter/flutter/issues/new?template=1_activation.md
✓ Flutter SDK: SDK Version : 3.7.11 is setup
✓ Project now uses Flutter SDK : SDK Version : 3.7.11
这个错误和Flutter或者Dart版本无关,而是MacOS自带的unzip
命令似乎有问题,会认为下载下来的.zip
包损坏,无法解压。解决方法也很简单:通过包管理器安装个unzip
,然后改用这个安装的unzip
即可。
我是用homebrew安装的,装完会提示需要修改PATH
,不然没法用。
我暂时放弃使用Nix做包管理器了,因为MacOS上似乎有很多复杂的细节,缺少相关文档/文章解释要怎么处理。反正这是工作机,只有我自己使用,所以homebrew虽然有多用户的权限问题,但对我来说不算问题。只不过homebrew执行命令时每次都要自己更新这事实在烦人。
另外,比较好玩的是在Finder中解包下载下来的.zip
包是没有问题的。所以我很不理解MacOS上的zip过程究竟是通过什么完成的,莫非是两个不同团队分别做了一份实现?
找不到Android SDK
由于需要编译Android的包,需要安装Android SDK。我选择通过homebrew安装官方的android-commandline
,用其提供的sdkmanager
来管理。这部分都挺顺利的,我也安装了相关版本的platform和build tools。
然而在Flutter编译时,会报告无法找到相应的Android SDK。似乎Flutter不能正确识别。
通过为Flutter手动设置Android SDK位置可以解决:
flutter config --android-sdk /opt/homebrew/share/android-commandlinetools
Java版本
做过Android开发会知道编译时需要特定(低)版本的Java,否则编译通不过。然而我系统全局使用的是几乎最新的版本,编译时会出问题。
解决方法是为项目设置Java版本。如果是较新版本Flutter的话(比如3.13.0
),这个设置可以通过Flutter进行:
flutter config --jdk-dir <path to jdk 15>
而如果是较早版本的Flutter,则没有--jdk-dir
这个参数,不能这么做,而只能通过在gradle.properties
中增加Java目录来解决(参考):
org.gradle.java.home=<path to jdk 15>
至于Java Home怎么找,则可以通过下面这个命令列出本机已经安装的所有环境,然后挑自己想要的(参考):
/usr/libexec/java_home -V
XCode SDK错误
我从来没有开发过Mac软件,所以在尝试编译成Mac程序时碰到了错误:
flutter macos xcrun: error: unable to find utility "xcodebuild", not a developer tool or in PATH
这个问题是因为我没有安装XCode,本来应该是很好解决的:去App Store安装就好了。
然而,由于我的系统是上一个版本,一直没有更新到最新,但App Store里的XCode只支持最新版本的系统,安装不了。
解决方法是手动下载XCode的历史版本,然后手动安装并部署。参考这里可以寻找到相关工具的历史版本,不过需要人工在页面中查找一下。
这里还是要稍微夸一下苹果的。虽然更好的解决方法是在App Store中提供相应版本的软件——毕竟我这只是差了一个版本,又不是上古版本。
通用问题
构建Android apk
在早期版本的Flutter工具中,似乎没有明确写怎么通过命令行构建特定平台的包。而且官方文档也写得不清不楚的,通篇都在写怎么构建release版本(需要配置自己的key),而不提怎么构建debug版本(不需要配置key)。
后来参考这个文章,猜出下面命令可以构建:
flutter build apk --debug
另注,较新版本的Flutter的
flutter build
命令的帮助写得清晰多了,直接告诉你可以flutter build apk
。但其实它还是没说debug build和release build的信息。
Firebase配置失败
我按照官方说明和其他人的文章去配置Firebase,但在执行flutterfire configure
时始终不成功,会报形如这样的错误:
i Firebase android app xxx registered.
i Firebase ios app xxx registered.
i Firebase macos app xxx registered.
i Firebase web app xxx (web) registered.
type 'Null' is not a subtype of type 'String' in type cast
在网上找到很多人也有这些报错(如[1][2]),但对解法都莫衷一是。我最初是发现它是和特定firebase目标平台有关(也就是web和windows),只要配置时不选中它们就可以正常执行结束,生成配置文件。
后来偶然发现,似乎是Dart版本问题:我在更新到Dart 3.3.4(Flutter 3.19.6)后,就再也不会出这个问题了。我也发现了这个SO问题,一直没人提到版本,所以我也凑了个热闹,去添了条评论。
顺便喷一下firebase:它不支持linux作为部署目标平台,不知道是为什么。
库问题
一些库会导致构建出错,比如我这里用到了video_compress
,在编译(apk)时会导致报下面这个错误:
Could not find com.otaliastudios:transcoder:0.9.1.
这个问题早就有人报告了,一个临时性解决方法是在pubspec.yml
中改用如下依赖声明:
video_compress:
git:
url: https://github.com/SpectoraSoftware/VideoCompress
ref: caf41183e84adebd6ef5c8fba547937daf556a65 # Or master?
另外,后来在更新到Flutter 3.22后,这个声明似乎也不再需要。但我不太理解为什么在更早的Flutter版本时会需要这句。
Dart和依赖版本
最后说个大的问题吧。我在配置的过程中发现,Dart的依赖和版本检测似乎挺成问题的,至少和直觉对不上号。
简单来说,它似乎可以自动进行下面的检测:
-
(在fvm那)根据你配置的Dart SDK版本(
sdk:
)判定要安装的Flutter版本是否合适当前项目; -
根据你配置的sdk版本,判定新增依赖是否可以使用;
-
检查各包间的版本依赖,并判断最新可以升级到哪个版本。
然而, 下面的这些是很成问题的:
- 配置中写了sdk版本,但
flutter pub upgrade
更新到的版本会因为用了更新的语言特性而导致编译不过; - 仓库中(
pub.dev
)的库声明的最低Dart版本有的不正确。
注意这其实是两个问题:1. 虽然声明了版本,但本地的Dart/Flutter工具链不知道因为什么不能正确判定可行版本;2. 仓库中的版本兼容性声明有问题。
第一点是工具本身的问题,我暂时没有发现规律,但在我的尝试中出现过。第二点则是维护的问题,不完全是语言本身的错误。然而我之所以对它们有很大意见,是因为有许多包都存在这种版本兼容性声明错误的问题,甚至包括谷歌自己的包(firebase)。这两点共同导致我在升级依赖时反复尝试,花了数个小时才解决。
这就让我不太理解了,毕竟语言特性这事很容易检查,第三方的包维护有问题也就算了,谷歌自己的包也有问题算是哪门子的事?难道谷歌firebase、flutter或者dart团队对发布包时没有兼容性质量检查环节?
再稍微延伸一下,其实很多语言都有这个问题:仓库中声明的兼容版本未必是真实的。许多人通过各种CI工具自动检查不同版本的依赖,所以原则上也许可以通过这种方式检查所有的?甚至更进一步,只是检查编译型语言特性的话,资源消耗应该比较小,也许仓库可以提供相关检查?
总结与感受
目前感受来说,Flutter设计有点意思,在跨平台的开发上似乎有不少的巧思,且由于显式地暴露个平台的脚手架代码,与已有生态有相当的兼容度(虽然代价是迁移时比较麻烦)。
但它的不少细节设计给我一种放任自流的感觉,并没有良好解决;另外还有一部分则由明显的公司业务倾向驱动,比如web构建执行只会找chrome(是的,甚至不含chromium),而且由于最近firefox不打算支持chrome的调试接口,现在有了更冠冕堂皇的不支持firefox的理由了(参见这个issue)。
我是很喜欢它的跨平台性的。当初那个年代需要开发额外的一门语言似乎也有一定道理。但我对Dart的一个设计深表不满:import
会导入目标的所有符号到当前命名空间,而不是将目标作为一个命名空间导入。默认行为会鼓励使用,这应当是众所周知的事情。而这种使用为重构和查错带来了大量麻烦,让人实在是不能理解为什么要这么设计。
总之,我会继续在这个项目上使用Flutter和Dart,希望之后可以给我带来更多的舒适而非目前碰到的各种复杂。
您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。