为了纪念(?)我的育儿假,简单介绍一下当时2025年的安卓应用开发环境。
用的是 Gradle 构建虽然提到了一些关于bazel的话题,但目前主流仍然是gradle。对于不是特别大或特别复杂的项目,不要让自己太累,还是用gradle好了。
构建设置:约定插件
现在更常见的是由一个应用模块和多个库模块组成的结构。这样一来,多个模块的 build.gradle
文件中就可能有相同的设置重复出现。以前,人们通常直接使用 apply from: common.gradle
这样的方式引用其他 Gradle 文件,但现在人们更倾向于通过创建约定插件(convention plugin)来优雅地共享这些配置。
依赖管理:版本清单
为了更好地管理多个模块中依赖库的版本,以前我们会采用构建配置或各种变通方法,但现在[[版本目录](https://docs.gradle.org/current/userguide/version_catalogs.html)]已经成为标准方法。
如果需要在多个模块中重复列出相同的依赖项,可以考虑使用约定插件或版本目录中的bundle功能来整洁地整理它们。在使用Compose的模块中,由于依赖项数量众多,可以在版本目录中添加一个Compose bundle,只需一行即可解决,或者创建一个Compose约定插件,将所有依赖项统一处理。
咱们来看看缓存构建的引入
Gradle 会缓存构建的结果,这叫作 构建缓存。如果模块没有发生变化,在下次构建时,Gradle 不会重新构建该模块,而是直接从缓存中获取输出,因此速度更快。
构建缓存可以分为本地缓存和远程缓存。如果你是独自开发,那么本地缓存就足够了;但如果是多人协作开发,引入远程缓存可以减少其他团队成员的构建时间。要配置远程缓存,你需要一个缓存服务器,由于提供了 build-cache-node 这个 Docker 镜像,所以可以轻松地进行配置。显然,这个 Docker 镜像的存储需要持久化保存在某个地方,这样才能真正发挥缓存的作用。
对于远程构建缓存,如果是较为保守的组织,可以规定只允许 CI 服务器向缓存服务器推送。
构建性能分析工具 : build-scan
通过使用 Gradle 的 构建扫描 功能,可以获得减少构建时间的提示。尽管 Android Studio 也有构建分析功能,但构建扫描提供的分析结果更细致。
问题是,build scan 的结果并不是在本地提供,而是在 Gradle 服务器上提供的。具体发送哪些内容,还需要查阅文档,可能会有安全风险。虽然它提供了删除报告等一些功能,但在公司环境中使用时,为了保证安全,最好仔细阅读相关文档。使用 Gradle 的付费解决方案 develocity 可以解决这些问题,但需要付费。
模块构成:按功能划分关于模块是按层划分还是按功能划分,有很多争议。如果有人反驳,就说这些是我的文章,所以我的观点是对的。当然,按功能划分时也会有共享模型,情况会比较复杂。总之,我会按功能来拆分模型。
典型的模块分离的好处包括:
- 可以使用 Kotlin 的
internal
访问修饰符来隐藏内部实现细节,可以利用构建缓存等方法来加快构建速度。
最近因为KMP(Kotlin 多平台)的关系,ktor(ktor)也渐渐被人们提及。但我对 retrofit 比较熟悉,所以还是倾向于继续使用 Retrofit。当然,如果想试试 KMP,也可以考虑使用ktor(ktor)试试。不过,我个人觉得哪个都不错。
如果使用 retrofit,自然就会用到 coroutine,再进一步,我会让返回的结果以 Result
形式返回。这可以通过引入 retrofit-adapters 来实现。
@GET(...)
suspend fun fetchUser() : Result<UserDto>
// 获取用户信息,返回用户数据对象的结果
全屏模式 退出全屏
JSON 映射关系 - Kotlinx 序列化库在调用API的过程中,不可避免地需要将JSON与对象进行映射,这时可以使用kotlinx serialization。在Kotlin初期,人们提到过moshi等工具,但是目前我认为没有选择moshi的必要。
如果你问是否不能使用Gson,建议不要使用。因为它使用反射,所以不会调用Kotlin的构造函数,这可能导致一些意想不到的问题。举个例子,假设你创建了如下DTO对象:
data class UserDto(
val first: String, // 名字
val last: String) { // 姓氏
val full: String = first + last // 完整姓名
}
全屏模式 / 退出全屏
使用 gson
将 { "first": "a", "last": "b" }
映射为 UserDto
,查看 full
变量的值,会发现其值为 null
而不是 ab
,这是因为构造函数没有被执行。不过,上面的内容其实不是一个很好的例子。最好不要在 DTO 中加入这样的 full
字段。
数据如果持续增长,可以考虑使用 room,否则可以使用 datastore。
datastore 提供了基于 shared preference 的实现和基于 Protobuf 的实现(详情如下:链接)。虽然 Protobuf 看起来更高级,但因为迁移时可能会遇到麻烦,我还是不会使用它。
DI - 币关于是否使用 DI 以及是否有必要使用 DI,有很多争论,但我认为 DI 是必要的,相比之下,我更支持 koin,而不是 hilt。由于 KMP 的原因,koin 得到了更多的青睐,但从 dagger 开始,一路经历了 hilt 的复杂配置,这种增加的复杂性是否真的有必要,值得怀疑。我也是放弃 dagger 的人(大弃用者...)。当然,如果有人反驳,这里是我的文章,我的观点就是正确的...
koin 在初期因为所有的配置都在运行时进行,确实存在在运行时崩溃的风险,但从注解开始,最近虽还处于测试阶段,已经有了intelliJ插件来逐步解决这些问题。虽然还不完美,但它提供了通过单元测试来验证的方法,我觉得这样还是不错的。
图片加载工具 - coil(coil)除了已经被视为古董的picasso之外,现在大概只剩下glide和coil了,但是glide的自定义难度较大,我觉得现在使用coil会更好一些。
UI - 编辑现在完全进入了Compose的世界。但是仍然会遇到意想不到的bug,有时问题微小但棘手。有时一些微妙的问题仍然需要在某些地方使用基于XML的视图。不过,多亏了强大的预览功能,UI开发变得更加轻松。
视图和视图模型之间的通信 - 通信流程关于MVVM和MVI等架构的讨论意见太多样化了,这里就不展开讲了。得益于Compose,现在ViewModel会暴露状态(比如使用StateFlow等),在View层订阅这些状态并在Composable中渲染,这是主流做法。当然,关于是否将Toast这样的副作用包含进状态等设计上的考量,仍然有很多需要思考的地方。
我用了好几年mavericks,但对于新项目,是否真的需要使用mavericks呢?
代码质量和维护工具 - ktlint, konsist如果有多个开发者,为了统一编码风格,最好引入一个类似ktlint的工具。可以在git的预提交钩子等地方使用它。
虽然我还没有试过,但用来检查项目是否违反架构规则的konsist看起来也很好。
单元测试:JUnit 4单元测试依旧基于Junit 4编写。同样,Robolectric的运行也依赖于Junit 4。因此,我并没有觉得非得使用Junit 5不可。
阅读了关于单元测试的书之后,我倾向于采用经典派单元测试的方法。如果要遵循经典派的方法,这就产生了一个问题:应该尽量避免使用mock等工具,而是要提供真实的实现。但是,如果实现是其他模块的内部类,那又该如何提供呢?幸运的是,从大约AGP 8.5版本开始,Kotlin开始支持测试固件,可以适当创建这些测试固件,这样测试代码之间就可以共享这些固件。为此需要在build.gradle
文件中添加android.experimental.enableTestFixturesKotlinSupport=true
选项,但是很难找到这个选项的说明。甚至在Android开发文档网站上也找不到相关说明。我又是从哪里找到的呢?
这并不是单元测试,不过最近也经常听到有关截图测试或基于Compose预览的测试方法的讨论。这看起来很有趣,我也想试一试。
共同學(xué)習(xí),寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章