距离上次发布的有关代码目录结构的文章 《掘金之路(八)页面代码粒度化管理》 已经快5年了,这些时间里我开发的项目一般都是基于这种目录结构,当然在此基础上也有所改进,这就是写这篇文章的原因了。
如果问我经历过的最复杂的项目是什么,我想应该是一个扫读笔项目,这个项目的复杂不在于功能上的难易,而是在于代码结构上的复杂。想象一下,这个项目有n多个客户,每个客户可能同时有几款不同型号的 Android 定制设备,有小方块屏的(类似手表屏幕大小),有长屏的(比如128*96等几个尺寸)、有宽屏的,Android 系统覆盖 Android 4.4 - Android 11,当然还会涉及到定制系统的许多调用差异(比如有些系统集成了扫描 OCR 能力,可以对外提供调用,有些没有,需要自己集成第三方 OCR 去实现),最后还有客户的部分定制化需求(比如界面或功能)。
问题来了,如果你负责开发维护这个项目,你怎么来搭建一个架构,要求满足以下3点:
1、代码目录结构要清晰,随着项目功能增长也能保持清晰,方便以后开发和维护;
2、能够最大化的复用相同代码,避免维护多份相同代码;
3、能够灵活的处理差异化需求代码
你可以先思考,不用着急,思考完再往下看。
思考中...
下面先贴上我的答案:PinDemoProject,当然你可能会有更好的答案,欢迎在评论里一起分享。
代码结构的配置逻辑在根目录的 build.gradle.kts,这里就不贴上全部配置代码了。接下来只简单介绍 flavor 的配置项信息及对应的效果,不会说到各个配置的具体实现,如果对实现有兴趣的话还是需要去看 build.gradle.kts 里的实现代码。
flavor 的配置项信息如下:
kotlin
/**
* flavor 参数配置:
* applicationId:应用ID,没有配置则使用默认值
* screenOrientation:屏幕方向
* supportFocus:是否支持焦点(有物理方向按键或确认键等功能键则表示支持)
* modules:指定要引入的业务模块以及差异化页面(pins-diff)
* - pinsVersion:引入 pins-${pinsVersion} 目录下的所有页面
* - diffPins:指定需要引入 pins-diff 目录下的差异化页面
* versionCode:版本号
* versionName:版本名称
* abiFilters:指定保留的ABI类型
* systemApiLevel:设备的系统版本
* signingConfig:签名配置
*/
private val flavorConfigs = mapOf(
"flavorA" to mapOf(
"screenOrientation" to "landscape",
"supportFocus" to false,
"modules" to mapOf(
"m_core" to emptyMap<String, Any>(),
"m_user" to mapOf(
//user 模块使用 v1 版本:
"pinsVersion" to "v1",
"diffPins" to emptyArray<String>()
),
),
"versionCode" to 1,
"versionName" to "1.0.0",
"systemApiLevel" to 21,
),
"flavorB" to mapOf(
"applicationId" to "cbfg.pin.flavorB",
"screenOrientation" to "landscape",
"supportFocus" to false,
"modules" to mapOf(
"m_core" to emptyMap<String, Any>(),
"m_user" to mapOf(
//user 模块使用 v2 版本:
"pinsVersion" to "v2",
//引入 pins-diff 中的 p_vip 页:
"diffPins" to arrayOf("p_vip")
),
),
"versionCode" to 1,
"versionName" to "1.0.0",
"systemApiLevel" to 19,
"signingConfig" to { appExtension: com.android.build.gradle.AppExtension ->
appExtension.signingConfigs.create("xxx") {
keyAlias = "xxx"
keyPassword = "xxx"
storeFile = file("xxx.jks")
storePassword = "xxx"
}
}
),
"flavorC" to mapOf(
"applicationId" to "cbfg.pin.flavorC",
"screenOrientation" to "portrait",
"supportFocus" to true,
"modules" to mapOf(
"m_core" to emptyMap<String, Any>(),
"m_user" to mapOf(
//user 模块使用 v3 版本:
"pinsVersion" to "v3",
//引入 pins-diff 中的 p_vip 页:
"diffPins" to arrayOf("p_vip")
),
),
"versionCode" to 1,
"versionName" to "1.0.0",
"systemApiLevel" to 19,
)
)
结合注释,上面的配置应该不难理解,每个 flavor 的配置都是独立的,关键的一个配置是 modules 配置,可以根据 flavor 需要去引入所需的模块及版本,同时可以额外去引入差异化目录中的页面(比如 flavorB 和 flavorC 的 p_vip 页是一致的,但是 flavorA 的是专属的,那么就把 flavorB 和 flavorC 的 p_vip 页提取到了差异化目录 pins-diff 中,flavorB 和 flavorC 额外去引入即可)
接下来再看下项目中 user 模块代码目录截图:

其中:
1、pins 目录是放置公用页面代码的,放置在这个目录内的页面代码默认会被引入;
2、pins-diff 是放置差异化页面代码的,需要在 flavor -> modules ->对应模块 -> diffPins 配置中加入才会被引入;
3、pins-v1、pins-v2、pins-v3 对应的是这个模块的3个不同版本,模拟不同 flavor 需要的不同版本,需要在 flavor -> modules -> 对应模块 -> pinsVersion 配置中指定才会引入对应的版本(pinsVersion 的命名可以是随意的,可以根据自己的需要去起名称,比如可以是 pins-flavorA,只要在 flavor 的配置中指定 pinsVersion 为 flavorA 即可);
从截图中还可以看到 pins 目录下有个 p_user_core 目录,pins-v3 目录下有个 p_user_ui 目录(其实 pins-v1、pins-v2 下也有 p_user_ui 这个目录),这里模拟的是3个版本中,p_user 页 ui 部分可能不同,但是核心逻辑是相同的,所以抽取了核心逻辑到 pins 目录下(对应 pins/p_user_core),而 ui 部分则保留在各自版本中(对应 pins-v1、pins-v2、pins-v3 目录里的 p_user_ui),这里只是为了说明配置能够灵活的复用公共代码并支持差异化。
介绍完毕,详细配置见:build.gradle.kts。