Android組件化開(kāi)發(fā)規(guī)范

進(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è)抽象的概念,moduleAndroid 項(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)的方案就是將 CD 其中的一個(gè),或者兩個(gè)都下沉到 common 組件中,因?yàn)?module-a-apimodule-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、DialogAdapter 等。除此之外,為了集中管理組件內(nèi)部資源和統(tǒng)一編碼習(xí)慣,特地將一部分的通用功能路徑固定下來(lái)。這些路徑包括 apiprovider、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
}

其中 nameicon 是從服務(wù)器獲取的字段,而 countisSelected 是客戶(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
    }
    ...
}

示例

這里介紹如何在外部 moduleuser-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"
}

注意:

  1. 緩存Key的命名必須用組件名作為前綴,防止緩存Key重復(fù)。
  2. 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-apimodule-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-implbuild.gradle 的頂部增加以下代碼來(lái)控制 user-implApplicatonLibrary 之間進(jìn)行切換:

if (isDebugModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

user-implsrc/main 的目錄下創(chuàng)建兩個(gè)文件夾 releasedebugrelease 中放 library 模式下的 AndroidManifest.xml,debugapplication 模式下的 AndroidManifest.xml、代碼和資源,如下圖所示:

user-implbuild.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-implbuild.gradle 中配置 applicationId

android {
    defaultConfig {
        if (isDebugModule.toBoolean()) {
            applicationId "cc.tarylorzhang.demo"
        }
        ...
    }
}

注意:如果碰到65536的問(wèn)題,在 user-implbuild.gradle 中新增以下配置:

android {
    defaultConfig {
        ...
        if (isDebugModule.toBoolean()) {
            multiDexEnabled true
        }
    }
}

以上工作都完成后,將 isDebugModule 的值改為 true,則可以開(kāi)始單獨(dú)調(diào)試用戶(hù)組件。

命名規(guī)范

module名

組件名如果是單個(gè)單詞的,直接使用該單詞 + apiimpl 的后綴作為 module 名,如果是多個(gè)單詞的,多個(gè)單詞小寫(xiě)使用 - 字符作為連接符,然后在其基礎(chǔ)上加 apiimpl 的后綴作為 module 名。

示例

用戶(hù)組件(User),它的 module 名為 user-apiuser-impl;會(huì)員卡組件(MembershipCard),它的 module 名為 membership-card-apimembership-card-impl

包名

在應(yīng)用的 applicationId 的基礎(chǔ)上增加組件名后綴作為組件基礎(chǔ)包名。

在代碼中的包名 module-apimodule-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)用的 applicationIdcc.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

commontool 路徑下創(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-implbuild.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 中放。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom閱讀 3,205評(píng)論 0 3
  • 概述 組件化緣由 記得剛開(kāi)始接觸Android開(kāi)發(fā)的時(shí)候,只知道MVC分層架構(gòu),而且感覺(jué)Model,View以及C...
    wustor閱讀 1,988評(píng)論 0 12
  • 概述 組件化緣由 記得剛開(kāi)始接觸Android開(kāi)發(fā)的時(shí)候,只知道MVC分層架構(gòu),而且感覺(jué)Model,View以及C...
    Simplelove_f033閱讀 842評(píng)論 0 0
  • 畸形的網(wǎng)紅文化,不堪的網(wǎng)絡(luò)直播,敗壞的社會(huì)風(fēng)氣,顛倒的是非觀念。當(dāng)今社會(huì)你說(shuō)的如果是對(duì)的,那么大部分人就會(huì)認(rèn)為它是...
    云端的ren閱讀 258評(píng)論 0 0
  • 消失的童年 杭州客 成人們努力于《創(chuàng)世紀(jì)》里的那句話(huà):“我將按我的想象來(lái)創(chuàng)造人”。成人們這種想替代上帝的...
    秦淮書(shū)生閱讀 359評(píng)論 0 1

友情鏈接更多精彩內(nèi)容