進(jìn)行組件化開(kāi)發(fā)有一段時(shí)間了,不久后就要開(kāi)始一個(gè)新項(xiàng)目了,為此整理了目前項(xiàng)目中使用的組件化開(kāi)發(fā)規(guī)范,方便在下一個(gè)項(xiàng)目上使用。本文的重點(diǎn)是介紹規(guī)范和項(xiàng)目架構(gòu),僅提供示例代碼舉例,目前不打算提供示例Demo。如果你還不了解什么是組件化以及如何進(jìn)行組件化開(kāi)發(fā)的話(huà),建議請(qǐng)先看其他組件化入門(mén)文章。
定義
組件是 Android 項(xiàng)目中一個(gè)相對(duì)獨(dú)立的功能模塊,是一個(gè)抽象的概念,module 是 Android 項(xiàng)目中一個(gè)相對(duì)獨(dú)立的代碼模塊。
在組件化開(kāi)發(fā)的早期,一個(gè)組件就只有一個(gè) module,導(dǎo)致很多代碼和資源都會(huì)下沉到 common 中,導(dǎo)致 common 會(huì)變得很臃腫。有的文章說(shuō),專(zhuān)門(mén)建立一個(gè) module 來(lái)存放通用資源,我感覺(jué)這樣是治標(biāo)不治本,直到后面看到微信Android模塊化架構(gòu)重構(gòu)實(shí)踐這篇文章,里面的"模塊的一般組織方式"一節(jié)提到一個(gè)模塊應(yīng)該有多個(gè)工程,然后開(kāi)始在項(xiàng)目對(duì) module 進(jìn)行拆分。
一般情況下,一個(gè)組件有兩個(gè) module,一個(gè)輕量級(jí)的 module 提供外部組件需要和本組件進(jìn)行交互的接口方法及一些外部組件需要的資源,另一個(gè)重量級(jí)的 module 完成組件實(shí)際的功能和實(shí)現(xiàn)輕量級(jí) module 定義的接口方法。
module 的命名規(guī)范請(qǐng)參考module名,在下文中使用 module-api 代表輕量級(jí)的 module,使用 module-impl 代表重量級(jí)的 module。
common組件
common 是一個(gè)特殊的組件,不區(qū)分輕量級(jí)和重量級(jí),它是項(xiàng)目中最底層的組件,基本上所有的其他組件都會(huì)依賴(lài) common 組件,common 中放項(xiàng)目中所有弱業(yè)務(wù)邏輯的代碼和解決循環(huán)依賴(lài)的代碼和資源。
一個(gè)完整的項(xiàng)目的架構(gòu)如下:

弱業(yè)務(wù)邏輯代碼
何為弱業(yè)務(wù)邏輯代碼?簡(jiǎn)單來(lái)說(shuō),就是有一定的業(yè)務(wù)邏輯,但是這個(gè)業(yè)務(wù)邏輯對(duì)于項(xiàng)目中其他組件來(lái)說(shuō)通用的。
比如在 common 組件集成網(wǎng)絡(luò)請(qǐng)求庫(kù),創(chuàng)建一個(gè) HttpTool 工具類(lèi),負(fù)責(zé)初始化網(wǎng)絡(luò)請(qǐng)求框架,定義網(wǎng)絡(luò)請(qǐng)求方法,實(shí)現(xiàn)組裝通用請(qǐng)求參數(shù)以及處理全局通用錯(cuò)誤等,對(duì)于其他組件直接通過(guò)這個(gè)工具類(lèi)進(jìn)行網(wǎng)絡(luò)請(qǐng)求就可以了。
比如定義界面基類(lèi),處理一些通用業(yè)務(wù)邏輯,比如接入統(tǒng)計(jì)分析框架。
解決循環(huán)依賴(lài)的代碼和資源
何為解決循環(huán)依賴(lài)的代碼和資源?比如說(shuō) module-a-api 有一個(gè)類(lèi) C,module-b-api 中有一個(gè)類(lèi) D,在 module-a-api 中需要使用 D,在 module-b-api 中需要使用 C,這樣就會(huì)造成 module-a-api 需要依賴(lài) module-b-api,而 module-b-api 也會(huì)依賴(lài) module-a-api,這就造成了循環(huán)依賴(lài),在 Android Studio 中會(huì)編譯失敗。
解決循環(huán)依賴(lài)的方案就是將 C 和 D 其中的一個(gè),或者兩個(gè)都下沉到 common 組件中,因?yàn)?module-a-api 和 module-b-api 都依賴(lài)了 common 組件,至于具體下沉幾個(gè),這個(gè)根據(jù)具體的情況而定,但是原則是下沉到 common 組件的東西越少越好。
上面的舉的例子是代碼,資源文件同樣也可能會(huì)有這個(gè)問(wèn)題。
module代碼結(jié)構(gòu)
一個(gè)組件通常含有一個(gè)或多個(gè)功能點(diǎn),比如對(duì)于用戶(hù)組件,它有關(guān)于界面、意見(jiàn)反饋、修改賬戶(hù)密碼等功能點(diǎn),在 module 中為每一個(gè)功能點(diǎn)創(chuàng)建一個(gè)路徑,里面放實(shí)現(xiàn)該功能的代碼,比如 Activity、Dialog 、Adapter 等。除此之外,為了集中管理組件內(nèi)部資源和統(tǒng)一編碼習(xí)慣,特地將一部分的通用功能路徑固定下來(lái)。這些路徑包括 api、provider、tool 等。
一般情況下 module 的代碼架構(gòu)如下圖:

api
該路徑下放 module 內(nèi)部使用到的所有網(wǎng)絡(luò)請(qǐng)求路徑和方法,一般使用一個(gè)類(lèi)就夠了,比如:UserApi:
object UserApi {
/**
* 獲取個(gè)人中心數(shù)據(jù)
*/
fun getPersonCenterData(): GetRequest {
return HttpTool.get(ApiVersion.v1_0_0 + "authUser/myCenter")
}
}
ApiVersion 全局管理目前項(xiàng)目中使用的所有 api 版本,應(yīng)當(dāng)定義在 common 組件的 api 路徑下:
object ApiVersion {
const val v1_0_0 = "v1/"
const val v1_1_0 = "v1_1/"
const val v1_2_2 = "v1_2_2/"
}
entity
該路徑下放 module 內(nèi)部使用到的所有實(shí)體類(lèi)(網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)類(lèi))。
對(duì)于所有從服務(wù)器獲取的字段,全部定義在構(gòu)造函數(shù)中,且實(shí)體類(lèi)應(yīng)當(dāng)實(shí)現(xiàn) Parcelable ,并使用 @Parcelize 注解。對(duì)于客戶(hù)端使用而自己定義的字段,基本上定義為普通成員字段,并使用 @IgnoredOnParcel 注解,如果需要在界面間傳遞客戶(hù)端定義的字段,可以將該字段定義在構(gòu)造函數(shù)中,但是必須注明是客戶(hù)端定義的字段。
示例如下:
@Parcelize
class ProductEntity(
// 產(chǎn)品名稱(chēng)
var name: String = "",
// 產(chǎn)品圖標(biāo)
var icon: String = "",
// 產(chǎn)品數(shù)量(客戶(hù)端定義字段)
var count: Int = 0
) : Parcelable {
// 用戶(hù)是否選擇本產(chǎn)品
@IgnoredOnParcel
var isSelected = false
}
其中 name 和 icon 是從服務(wù)器獲取的字段,而 count 和 isSelected 是客戶(hù)端自己定義的字段。
event
該路徑下放 module 內(nèi)部使用的事件相關(guān)類(lèi)。對(duì)于使用了 EventBus 及類(lèi)似框架的項(xiàng)目,放事件類(lèi),對(duì)于使用了 LiveEventBus 的項(xiàng)目,里面只需要放一個(gè)類(lèi)就好,比如:UserEvent:
object UserEvent {
/**
* 更新用戶(hù)信息成功事件
*/
val updateUserInfoSuccessEvent: LiveEventBus.Event<Unit>
get() = LiveEventBus.get("user_update_user_info_success")
}
注意:對(duì)于使用
LiveEventBus的項(xiàng)目,事件的命名必須用組件名作為前綴,防止事件名重復(fù)。
route
該路徑下放 module 內(nèi)部所使用到的界面路徑和跳轉(zhuǎn)方法,一般使用一個(gè)類(lèi)就夠了,比如:UserRoute:
object UserRoute {
// 關(guān)于界面
const val ABOUT = "/user/about"
// 常見(jiàn)問(wèn)題(H5)
private const val FAQ = "FAQ/"
/**
* 跳轉(zhuǎn)至關(guān)于界面
*/
fun toAbout(): RouteNavigation {
return RouteNavigation(ABOUT)
}
/**
* 跳轉(zhuǎn)至常見(jiàn)問(wèn)題(H5)
*/
fun toFAQ(): RouteNavigation? {
return RouteUtil.getServiceProvider(IH5Service::class.java)
?.toH5Activity(FAQ)
}
}
注意:對(duì)于組件內(nèi)部會(huì)跳轉(zhuǎn)的H5界面鏈接也應(yīng)當(dāng)寫(xiě)在路由類(lèi)中。
provider
該路徑下放對(duì)外部 module 提供的服務(wù),一般使用一個(gè)類(lèi)就夠了。在 module-api 中是一個(gè)接口類(lèi),在 module-impl 中是該接口類(lèi)的實(shí)現(xiàn)類(lèi)。
目前采用 ARouter 作為組件化的框架,為了解耦,對(duì)其進(jìn)行了封裝,封裝示例代碼如下:
typealias Route = com.alibaba.android.arouter.facade.annotation.Route
object RouteUtil {
fun <T> getServiceProvider(service: Class<out T>): T? {
return ARouter.getInstance().navigation(service)
}
}
class RouteNavigation(path: String) {
private val postcard = ARouter.getInstance().build(path)
fun param(key: String, value: Int): RouteNavigation {
postcard.withInt(key, value)
return this
}
...
}
示例
這里介紹如何在外部 module 和 user-impl 跳轉(zhuǎn)至用戶(hù)組件中的關(guān)于界面。
準(zhǔn)備工作
在 user-impl 中創(chuàng)建路由類(lèi),編寫(xiě)關(guān)于界面的路由和服務(wù)路由及跳轉(zhuǎn)至關(guān)于界面方法:
object UserRoute {
// 關(guān)于界面
const val ABOUT = "/user/about"
// 用戶(hù)組件服務(wù)
const val USER_SERVICE = "/user/service"
/**
* 跳轉(zhuǎn)至關(guān)于界面
*/
fun toAbout(): RouteNavigation {
return RouteNavigation(ABOUT)
}
}
在關(guān)于界面使用路由:
@Route(path = UserRoute.ABOUT)
class AboutActivity : MyBaseActivity() {
...
}
在 user-api 中定義跳轉(zhuǎn)界面方法:
interface IUserService : IServiceProvider {
/**
* 跳轉(zhuǎn)至關(guān)于界面
*/
fun toAbout(): RouteNavigation
}
在 user-impl 中實(shí)現(xiàn)跳轉(zhuǎn)界面方法:
@Route(path = UserRoute.USER_SERVICE)
class UserServiceImpl : IUserService {
override fun toAbout(): RouteNavigation {
return UserRoute.toAbout()
}
}
界面跳轉(zhuǎn)
在 user-impl 中可以直接跳轉(zhuǎn)到關(guān)于界面:
UserRoute.toAbout().navigation(this)
假設(shè) module-a 需要跳轉(zhuǎn)到關(guān)于界面,那么先在 module-a 中配置依賴(lài):
dependencies {
...
implementation project(':user-api')
}
在 module-a 中使用 provider 跳轉(zhuǎn)到關(guān)于界面:
RouteUtil.getServiceProvider(IUserService::class.java)
?.toAbout()
?.navigation(this)
module依賴(lài)關(guān)系
此時(shí)各個(gè) module 的依賴(lài)關(guān)系如下:
common:基礎(chǔ)庫(kù)、第三方庫(kù)
user-api:common
user-impl:common、user-api
module-a:common、user-api
App殼:common、user-api、user-impl、module-a、...
tool
該路徑下放 module 內(nèi)部使用的工具方法,一般一個(gè)類(lèi)就夠了,比如:UserTool:
object UserTool {
/**
* 該用戶(hù)是否是會(huì)員
* @param gradeId 會(huì)員等級(jí)id
*/
fun isMembership(gradeId: Int): Boolean {
return gradeId > 0
}
}
cache
該路徑下放 module 使用的緩存方法,一般一個(gè)類(lèi)就夠了,比如:UserCache:
object UserCache {
// 搜索歷史記錄列表
var searchHistoryList: ArrayList<String>
get() {
val cacheStr = CacheTool.userCache.getString(SEARCH_HISTORY_LIST)
return if (cacheStr == null) {
ArrayList()
} else {
JsonUtil.parseArray(cacheStr, String::class.java) ?: ArrayList()
}
}
set(value) {
CacheTool.userCache.put(SEARCH_HISTORY_LIST, JsonUtil.toJson(value))
}
// 搜索歷史記錄列表
private const val SEARCH_HISTORY_LIST = "user_search_history_list"
}
注意:
- 緩存Key的命名必須用組件名作為前綴,防止緩存Key重復(fù)。
CacheTool.userCache并不是指用戶(hù)組件的緩存,而是用戶(hù)的緩存,即當(dāng)前登錄賬號(hào)的緩存,每個(gè)賬號(hào)會(huì)單獨(dú)存一份數(shù)據(jù),相互之間沒(méi)有干擾。與之對(duì)應(yīng)的是CacheTool.globalCache,全局緩存,所有的賬號(hào)會(huì)共用一份數(shù)據(jù)。
兩種module的區(qū)別
module-api 中放的都是外部組件需要的,或者說(shuō)外部組件和 module-impl 都需要的,其他的都應(yīng)當(dāng)放在 module-impl 中,對(duì)于外部組件需要的但是能通過(guò) provider 方式提供的,都應(yīng)當(dāng)把具體的實(shí)現(xiàn)放在 module-impl 中,module-api 中只是放一個(gè)接口方法。
下表列舉項(xiàng)目開(kāi)發(fā)中哪些東西能否放 module-api 中:
| 類(lèi)型 | 能否放 module-api
|
備注 |
|---|---|---|
| 功能界面(Activity、Fragment、Dialog) | 不能 | 通過(guò) provider 方式提供使用 |
| 基類(lèi)界面 | 部分能 | 外部 module 需要使用的可以,其他的放 module-impl 中 |
| adapter | 部分能 | 外部 module 需要使用的可以,其他的放 module-impl 中 |
| provider | 部分能 | 只能放接口類(lèi),實(shí)現(xiàn)類(lèi)放 module-impl 中 |
| tool | 部分能 | 外部 module 需要使用的可以,其他的放 module-impl 中 |
| api、route、cache | 不能 | 通過(guò) provider 方式提供使用 |
| entity | 部分能 | 外部 module 需要使用的可以,其他的放 module-impl 中 |
| event | 部分能 | 對(duì)使用 EventBus 及類(lèi)似框架的項(xiàng)目,外部組件需要的可以,其他還是放 module-impl 中對(duì)于使用了 LiveEventBus 的項(xiàng)目不能,通過(guò) provider 方式提供使用 |
| 資源文件和資源變量 | 部分能 | 需要在 xml 文件中使用的可以, 其他的通過(guò) provider 方式提供使用 |
注意:如果僅在
module-impl中存在工具類(lèi),則該工具類(lèi)命名為xxTool。如果module-api和module-impl都存在工具類(lèi),則module-api中的命名為xxTool,module-impl中的命名為xxTool2。
組件單獨(dú)調(diào)試
在開(kāi)發(fā)過(guò)程中,為了查看運(yùn)行效果,需要運(yùn)行整個(gè)App,比較麻煩,而且可能依賴(lài)的其他組件也在開(kāi)發(fā)中,App可能運(yùn)行不到當(dāng)前開(kāi)發(fā)的組件。為此可以采用組件單獨(dú)調(diào)試的模式進(jìn)行開(kāi)發(fā),減少其他組件的干擾,等開(kāi)發(fā)完成后再切換回 library 的模式。
在組件單獨(dú)調(diào)試模式下,可以增加一些額外的代碼來(lái)方便開(kāi)發(fā)和調(diào)試,比如新增一個(gè)入口 Actvity,作為組件單獨(dú)運(yùn)行時(shí)的第一個(gè)界面。
示例
這里介紹在 user-impl 中進(jìn)行組件單獨(dú)調(diào)試。
在項(xiàng)目根目錄下的 gradle.properties 文件中新增變量 isDebugModule,通過(guò)該變量控制是否進(jìn)行組件單獨(dú)調(diào)試:
# 組件單獨(dú)調(diào)試開(kāi)關(guān),為ture時(shí)進(jìn)行組件單獨(dú)調(diào)試
isDebugModule = false
在 user-impl 的 build.gradle 的頂部增加以下代碼來(lái)控制 user-impl 在 Applicaton 和 Library 之間進(jìn)行切換:
if (isDebugModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
在 user-impl 的 src/main 的目錄下創(chuàng)建兩個(gè)文件夾 release 和 debug,release 中放 library 模式下的 AndroidManifest.xml,debug 放 application 模式下的 AndroidManifest.xml、代碼和資源,如下圖所示:

在 user-impl 的 build.gradle 中配置上面的創(chuàng)建的代碼和資源路徑:
android {
...
sourceSets {
if (isDebugModule.toBoolean()) {
main.manifest.srcFile 'src/main/debug/AndroidManifest.xml'
main.java.srcDirs += 'src/main/debug'
main.res.srcDirs += 'src/main/debug'
} else {
main.manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}
注意:完成上述配置后,在
library模式下,debug中的代碼和資源不會(huì)合并到項(xiàng)目中。
最后在 user-impl 的 build.gradle 中配置 applicationId:
android {
defaultConfig {
if (isDebugModule.toBoolean()) {
applicationId "cc.tarylorzhang.demo"
}
...
}
}
注意:如果碰到65536的問(wèn)題,在 user-impl 的 build.gradle 中新增以下配置:
android {
defaultConfig {
...
if (isDebugModule.toBoolean()) {
multiDexEnabled true
}
}
}
以上工作都完成后,將 isDebugModule 的值改為 true,則可以開(kāi)始單獨(dú)調(diào)試用戶(hù)組件。
命名規(guī)范
module名
組件名如果是單個(gè)單詞的,直接使用該單詞 + api 或 impl 的后綴作為 module 名,如果是多個(gè)單詞的,多個(gè)單詞小寫(xiě)使用 - 字符作為連接符,然后在其基礎(chǔ)上加 api 或 impl 的后綴作為 module 名。
示例
用戶(hù)組件(User),它的 module 名為 user-api 和 user-impl;會(huì)員卡組件(MembershipCard),它的 module 名為 membership-card-api 和 membership-card-impl。
包名
在應(yīng)用的 applicationId 的基礎(chǔ)上增加組件名后綴作為組件基礎(chǔ)包名。
在代碼中的包名 module-api 和 module-impl 都直接使用基礎(chǔ)包名即可,但是在 Android 中項(xiàng)目 AndroidManifest.xml 文件中的 package 不能重復(fù),否則編譯不通過(guò)。所以 module-impl 中的 package 使用基礎(chǔ)包名,而 module-impl 中的 package 使用基礎(chǔ)包名 + api 后綴。
package 重復(fù)的時(shí)候,會(huì)報(bào) Type package.BuildConfig is defined multiple times 的錯(cuò)誤。
示例
應(yīng)用的 applicationId 為 cc.taylorzhang.demo,對(duì)于用戶(hù)組件(user),組件基礎(chǔ)包名為 cc.taylorzhang.demo.user,則實(shí)際包名如下表:
| 代碼中的包名 | AndroidManifest.xml中的包名 | |
|---|---|---|
user-api |
cc.taylorzhang.demo.user |
cc.taylorzhang.demo.userapi |
user-impl |
cc.taylorzhang.demo.user |
cc.taylorzhang.demo.user |
對(duì)于多單詞的會(huì)員卡組件(MembershipCard),其組件基礎(chǔ)包名為 cc.taylorzhang.demo.membershipcard。
資源文件和資源變量
所有的資源文件:布局文件、圖片等全部要增加組件名作為前綴,所有的資源變量:字符串、顏色等也全部要增加組件名作為前綴,防止資源名重復(fù)。
示例
- 用戶(hù)組件(
User),關(guān)于界面布局文件命名為:user_activity_about.xml; - 用戶(hù)組件(
User),關(guān)于界面標(biāo)題字符串命名為:user_about_title; - 會(huì)員卡組件(
MembershipCard),會(huì)員卡詳情界面布局文件,文件名為:membership_card_activity_detail; - 會(huì)員卡組件(
MembershipCard),會(huì)員卡詳情界面標(biāo)題字符串,文件名為:membership_card_detail_title;
類(lèi)名
對(duì)于類(lèi)名沒(méi)必要增加前綴,比如 UserAboutActivity,因?yàn)閷?duì)資源文件和資源變量增加前綴主要是為了避免重復(fù)定義資源導(dǎo)致資源被覆蓋的問(wèn)題,而上面的包名命名規(guī)范已經(jīng)避免了類(lèi)重復(fù)的問(wèn)題,直接命名 AboutActivity 即可。
全局管理App環(huán)境
App 環(huán)境一般分為開(kāi)發(fā)、測(cè)試和生產(chǎn)環(huán)境,不同環(huán)境下使用的網(wǎng)絡(luò)請(qǐng)求地址大概率是不一樣的,甚至一些UI都不一樣,在打包的時(shí)候手動(dòng)修改很容易有遺漏,產(chǎn)生不必要的 BUG。應(yīng)當(dāng)使用 buildConfigField 在打包的時(shí)候?qū)?dāng)前環(huán)境寫(xiě)入 App 中,在代碼中根據(jù)讀取環(huán)境變量,根據(jù)不同的環(huán)境執(zhí)行不同的操作。
示例
準(zhǔn)備工作
在 App 殼 的 build.gradle 中給每個(gè)buildType 都配置 APP_ENV:
android {
...
buildTypes {
debug {
buildConfigField "String", "APP_ENV", '\"dev\"'
...
}
release {
buildConfigField "String", "APP_ENV", '\"release\"'
...
}
ctest {
initWith release
buildConfigField "String", "APP_ENV", '\"test\"'
matchingFallbacks = ['release']
}
}
}
注意:測(cè)試環(huán)境的
buildType不能使用test作為名字,Android Studio會(huì)報(bào)ERROR: BuildType names cannot start with 'test',這里在test前增加了一個(gè)c。
在 common 的 tool 路徑下創(chuàng)建一個(gè)App環(huán)境工具類(lèi):
object AppEnvTool {
/** 開(kāi)發(fā)環(huán)境 */
const val APP_ENV_DEV = "dev"
/** 測(cè)試環(huán)境 */
const val APP_ENV_TEST = "test"
/** 生產(chǎn)環(huán)境 */
const val APP_ENV_RELEASE = "release"
/** 當(dāng)前App環(huán)境,默認(rèn)為開(kāi)發(fā)環(huán)境 */
private var curAppEnv = APP_ENV_DEV
fun init(env: String) {
curAppEnv = env
}
/** 當(dāng)前是否處于開(kāi)發(fā)環(huán)境 */
val isDev: Boolean
get() = curAppEnv == APP_ENV_DEV
/** 當(dāng)前是否處于測(cè)試環(huán)境 */
val isTest: Boolean
get() = curAppEnv == APP_ENV_TEST
/** 當(dāng)前是否處于生產(chǎn)環(huán)境 */
val isRelease: Boolean
get() = curAppEnv == APP_ENV_RELEASE
}
在 Application 中初始化App環(huán)境工具類(lèi):
class DemoApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化App環(huán)境工具類(lèi)
AppEnvTool.init(BuildConfig.APP_ENV)
...
}
}
使用App環(huán)境工具類(lèi)
這里介紹根據(jù)App環(huán)境使用不同的網(wǎng)絡(luò)請(qǐng)求地址:
object CommonApi {
// api開(kāi)發(fā)環(huán)境地址
private const val API_DEV_URL = "https://demodev.taylorzhang.cc/api/"
// api測(cè)試環(huán)境地址
private const val API_TEST_URL = "https://demotest.taylorzhang.cc/api/"
// api生產(chǎn)環(huán)境地址
private const val API_RELEASE_URL = "https://demo.taylorzhang.cc/api/"
// api地址
val API_URL = getUrlByEnv(API_DEV_URL, API_TEST_URL, API_RELEASE_URL)
// H5開(kāi)發(fā)環(huán)境地址
private const val H5_DEV_URL = "https://demodev.taylorzhang.cc/m/"
// H5測(cè)試環(huán)境地址
private const val H5_TEST_URL = "https://demotest.taylorzhang.cc/m/"
// H5生產(chǎn)環(huán)境地址
private const val H5_RELEASE_URL = "https://demo.taylorzhang.cc/m/"
// H5地址
val H5_URL = getUrlByEnv(H5_DEV_URL, H5_TEST_URL, H5_RELEASE_URL)
private fun getUrlByEnv(devUrl: String, testUrl: String, releaseUrl: String): String {
return when {
AppEnvTool.isDev -> devUrl
AppEnvTool.isTest -> testUrl
else -> releaseUrl
}
}
}
打包
通過(guò)不同的命令打包,打出對(duì)應(yīng)的App環(huán)境包:
# 打開(kāi)發(fā)環(huán)境包
./gradlew clean assembleDebug
# 打測(cè)試環(huán)境包
./gradlew clean assembleCtest
# 打生產(chǎn)環(huán)境包
./gradlew clean assembleRelease
全局管理版本信息
項(xiàng)目中的 module 變多之后,如果要修改第三方庫(kù)和App使用的SDK版本是一件很蛋疼的事情。應(yīng)當(dāng)建立一個(gè)配置文件進(jìn)行管理,其他地方使用配置文件中設(shè)置的版本。
示例
在項(xiàng)目根目錄下創(chuàng)建一個(gè)配置文件 config.gradle,里面放版本信息:
ext {
compile_sdk_version = 28
min_sdk_version = 17
target_sdk_version = 28
arouter_compiler_version = '1.2.2'
}
在項(xiàng)目根目錄下的 build.gradle 文件中的最上方使用以下代碼引入配置文件:
apply from: "config.gradle"
創(chuàng)建 module 后,修改該 module 中的 build.gradle 文件,將 SDK 版本默認(rèn)值換成配置文件中的變量,按需添加第三方依賴(lài),并使用 $ + 配置文件中的變量作為第三方庫(kù)的版本:
android {
...
compileSdkVersion compile_sdk_version
defaultConfig {
...
minSdkVersion min_sdk_version
targetSdkVersion target_sdk_version
}
}
dependencies {
...
kapt "com.alibaba:arouter-compiler:$arouter_compiler_version"
}
混淆
混淆文件不應(yīng)該在 App 殼中集中定義,應(yīng)當(dāng)在每個(gè) module 中各自定義自己的混淆。
示例
這里介紹配置 user-impl 的混淆,先在 user-impl 的 build.gradle 中配置消費(fèi)者混淆文件:
android {
defaultConfig {
...
consumerProguardFiles 'proguard-rules.pro'
}
}
在 proguard-rules.pro 文件中寫(xiě)入該 module 的混淆:
# 實(shí)體類(lèi)
-keepclassmembers class cc.taylorzhang.demo.user.entity.** { *; }
總結(jié)
組件化開(kāi)發(fā)應(yīng)當(dāng)遵守"高內(nèi)聚,低耦合"的原則,盡量少的對(duì)外暴露細(xì)節(jié)。如果用一句話(huà)來(lái)總結(jié)的話(huà),就是代碼和資源能放 module-impl 里面的就都放在 module-impl,因?yàn)榇a隔離問(wèn)題實(shí)在不能放 module-impl 里面的才放 module-api,最后因?yàn)樯婕暗窖h(huán)依賴(lài)問(wèn)題的才往 common 中放。