本文为现代化 Android 开发系列文章第四篇。

完好目录为:

  • 现代化 Android 开发:根底架构
  • 现代化 Android 开发:数据类
  • 现代化 Android 开发:逻辑层
  • 现代化 Android 开发:组件化与模块化的抉择(本文)
  • 现代化 Android 开发:多 Activity 多 Page 的 UI 架构
  • 现代化 Android 开发:Jetpack Compose 最佳实践
  • 现代化 Android 开发:性能监控

项目初始的时分,一般都是使用一个分层架构,接入各种结构,然后就写事务代码。但假如项目渐渐变大,那就会呈现许多项目办理的问题,诸如:

  1. 代码复用与笼统问题
  2. 编译速度问题
  3. 版别迭代速度问题

所以组件化、模块化、动态化、插件化、跨渠道等各种技能就会被提及被重视。

组件化

我们现已度过了洪荒时期的 Android 开发,由于官方现已给出了十分优异的 Jetpack 组件库,所以我们现在更多的是制作事务型的组件与笼统,去处理某一类场景或者大家都或许用到而写起来又繁琐又简略出错的场景。

写组件也不是一件简略的事情。

首要,开发组件得意识到某些场景是能够笼统的,假如抱着代码能跑起来就行了的情绪去编程,那根本不或许想到去笼统的,也是最简略堕入代码陷阱无法自拔的。即使是最简略的抽取,许多时分也是甘愿不厌其烦的复制粘贴,也不愿意去抽取一个公共方法。然后等他人开发出了组件后,除了感叹真香,也就没什么其它感悟了。

其次,开发组件得规划出好用而简练的 API,许多童鞋,除了数据实体类和各种 Manager 类,根本不会再界说新的数据结构了,就更别提规划接口供其它人使用了。

然后,开发组件得想好组件的配置,提高组件的灵活性,产品最喜欢复用曾经的逻辑而再加上一点点的不同,假如不能够配置,那组件的局限性就大打折扣了。

最后,开发组件还得做好边际情况的处理、异常的处理。组件会被各种人以各种姿态调用,所以各种想不到的场景都有或许呈现,这也考验开发写代码对细节的把控。

所以我一直重视的是写组件的能力,是需求不断的练习的,也是需求不断的阅读优异的组件库去掌握的。面试必问 Okhttp 的拦截器,能够你有真实在自己的组件里使用过相似的东西吗?

最举个比如? 假定现在我们要用 AudioRecord 写一个录制功用。最开端那或许直接在 Activity 里开撸了:

private var isRecording = false;
private suspend fun startRecording() {
    audioRecord = AudioRecord(...)
    val data = ByteArray(bufferSize)
    audioRecord.startRecording()
    isRecording = true
    File(applicationContext.filesDir, "recording.pcm").outputStream().use {outputStream ->
        while (isRecording) {
            val bytesRead = audioRecord.read(data, 0, bufferSize)
            if (bytesRead > 0) {
                outputStream.write(data, 0, bytesRead)
            }
        }
    }
    audioRecord.stop()
    audioRecord.release()
}

这样写下来,流程一通,然后就觉得功德圆满了。

然后产品逻辑就改变了:要把音频数据实时上传到服务器。

改:把写文件的部分换成网络传输

过一会儿,老板又找来了,在没网络的时分录制就失利了,这不可,要在无网络的时分录制到本地。

改:判别下网络环境,加 if else

过一会儿,老板又找来了,我开端有网络,可是进入电梯后录制就失利了,所以一定要保存一份到文件中,假如有网络,那也往网络写一份。

改:从 if else 逻辑换成双写逻辑。

只针对上面的场景,全体流程是适当安稳的,仅仅写入的方针地不一样,那为何不笼统一个 AudioSink 的接口来表明写入的存储方针?

interface AudioSink {
    suspend fun write(buf: ByteArray, offset: Int, len: Int)
    fun close()
}

那整个流程就安稳为:

private suspend fun startRecording(sink: AudioSink) {
    audioRecord = AudioRecord(...)
    val data = ByteArray(bufferSize)
    audioRecord.startRecording()
    isRecording = true
    while (isRecording) {
        val bytesRead = audioRecord.read(data, 0, bufferSize)
        if (bytesRead > 0) {
            sink.write(data, 0, bytesRead)
        }
    }
    audioRecord.stop()
    audioRecord.release()
    sink.close()
}

假如要写文件,写个完成:

class FileAudioSink(file: File): AudioSink {
    ...
}

假如要写网络,写个完成:

class NetworkAudioSink(api): AudioSink {
    ...
}

假如要判别本地与网络,就写个代理:

class DelegateAudioSink(
    val origin: AudioSink
): AudioSink {
    ...
}

假如要一起写本地与网络,便是双写或多写:

class MultiAudioSink(
    val list: List<AudioSink>
): AudioSink {
    ...
}

这些类一写,那具体逻辑便是入口传参的改变了。当然实际情况应该考虑各种异常情况。假如你满足有经历,那在一开端就能把这些场景考虑到,并且把其用处和局限性同步给产品,协助产品一开端做出最优挑选。这才是所谓的经历。

假如回头来看上面的比如,发现都是很熟悉的东西,说是规划形式也行。假如将整个笼统做好,假如从音频切换到视频,那是否这一块能够完美复用?那是不是就能够更早的完成需求?——然后就能够更早的被裁掉~

组件化,便是对事务场景、功用场景的笼统。这是最需求大家去投入时刻去做、去练习的。

模块化

一些杂乱的组件,本身就能够构成一个个的模块,然后独自出来保护。但在议论模块的时分,更多的是议论事务模块。便是把不同事务放在不同的模块下。这关于巨型 App, 其含义当然是有的,毕竟或许是一个团队保护某一个模块。而关于小 App, 那含义就不是特别大了。

由于模块化会带来十分棘手的问题:模块之间的通信问题。ArouterTheRouter 都很大程度上是为了处理模块间的通信问题。而简略的模块化一般是考虑根底模块的抽取,但根底模块的界说总是含糊的,不经意间就抽取出一个巨无霸的根底模块。就跟许多喜欢用 Base 类的同学,最后就一个巨大的 Base 类。

所以一般小的 App, 搞模块化是大可不必的。巨型 App 是存在编译速度、多团队开发的问题,所以各大公司都会出品和分享自己的多模块化实践。小公司就简略跟风把工程搞得巨杂乱,这就没意思了。

例如大公司拆分多模块,然后用 aar 包来加速构建速度,相对应的有私有 Maven 仓库,自动打包脚本,搞得很是杂乱,对他们而言是很有必要。而关于小公司,其投入产出比,还不如花点钱,买台好一点的电脑来得香。

动态化、插件化

关于动态化和插件化,我只能感叹下,写出这些结构的大佬们,技能是真的强,都是真实的黑客。大约也只要国内的巨型 App 和奇怪的商场审核逻辑才干造就出这些需求。

有时刻的话,看看这些库的源码也是很不错的,很涨才智。重要的仍是看各大结构遇到的问题场景,以及是怎么探索处理计划,最终是怎么处理的。

不过问题始终是保护问题,随着 Android 版别的迭代,总之是需求有新的改动或者需求寻求新的计划,但国内的结构和库的成功往往和推动这个项目的人有关,没有一个安稳保护的机制,很简略受到人员变化的影响。

最后

再杂乱的东西,也是由一个个的零件渐渐拼装起来的。我们要有蓝图,然后为完成这个蓝图而拧螺丝。拧得多了,也许就有时机拧更大的螺丝。我在 emo 组件库上拧了 20 个螺丝,拧得多了,也算是一个不小的库了。不过现在我源码现已转 private 了,后面拧的螺丝,只想留给小团体顾影自怜了。用举动证明一下,国内的开发者的开源库是多么的不安稳。