Android-模塊化-項目實踐和探索分享

@TOC


前言

提示:這里需要提前對Android-模塊化-基本知識了解
本文主要分享個人在項目中實現(xiàn)Android模塊化中的gradle統(tǒng)一配置字旭、nexus肘习、maven-publish芳绩、動態(tài)依賴缤弦、模塊通信等思路


一年叮、gradle統(tǒng)一配置

1. 多模塊項目的構(gòu)建

settings.gradle 是根模塊項目以及模塊描述文件砂轻,include '模塊路徑(分隔符是冒號)' 或如下別名引入

include 'VScreen_App' //不建議有冒號
project(":VScreen_App").projectDir = file("VScreen") //指定真實模塊路徑

include 子模塊技巧 胃珍,如下

def sub_father = ':' //子項目父工程名, 更為了能Find Usages

//基礎(chǔ)組件庫
sub_father = ':--base_modules'
include '',
        "$sub_father:lib_arouter", //阿里路由
        "$sub_father:lib_baseAndroid", //安卓基礎(chǔ)api
        "$sub_father:lib_comm_ui", // ui組件庫
        "$sub_father:lib_component", // 常用組件庫
        "$sub_father:lib_export_table_java", // export_table組件庫
        "$sub_father:lib_glide", // img_glide
        "$sub_father:lib_okhttp", // net_okhttp
        //"$sub_father:lib_zxing",
        ''

業(yè)務(wù)模塊過多辰晕,include 業(yè)務(wù)模塊技巧 蛤迎,約定在指定目錄如下

//業(yè)務(wù)模塊
def business_modules_name = new ArrayList<String>()
def business_modules_symbol = new ArrayList<String>()
for (f in file("business_modules").listFiles()) {
    if (f.isDirectory() && new File(f, "build.gradle").exists()) {
        def name = ":business_modules:${f.name}"
        business_modules_name.add("${name}")
        business_modules_symbol.add("'${name}'")
    }
}
//業(yè)務(wù)模塊動態(tài)添加 (考慮的業(yè)務(wù)模塊有很多)
def business_modules_dynamically_add = true
if (business_modules_dynamically_add) {
    //動態(tài)添加目錄底下所有
    business_modules_name.forEach {
        include(it)
    }
} else {
    //手動按需添加
    def include_business_modules_str = "include '',\n"
    business_modules_symbol.forEach {
        include_business_modules_str += "$it,\n"
    }
    include_business_modules_str += "''"
    println "輸出include腳本, 按需開啟\n" + include_business_modules_str + "\n輸出include腳本, 按需開啟"

    //Gradle窗口: 輸出include腳本, 按需開啟
    //include '',
    //        ':business_modules:lib_attendance',
    //        ':business_modules:lib_consume',
    //        ':business_modules:lib_family_phone',
    //        ''

}

println "> Configure 業(yè)務(wù)模塊 : ${business_modules_symbol}"

老項目工程龐大臃腫,一時無法分離含友。 一般我們會把這個app工程轉(zhuǎn)化為核心庫(下沉給其它工程依賴使用)替裆,添加新的殼工程校辩。我們能不能做到不需要空殼app ?答案是肯定的

build.gradle 描述子模塊的項目的插件辆童、屬性宜咒、依賴等“鸭可以在settings.gradle 中自定義腳本文件名

project(":VScreen").buildFileName = "lib_core.gradle"  //改變腳本一個工程打兩份工,實測ojbk

Gradle Event Log 提示重復(fù)工程故黑,不友好,但是能節(jié)省了一個無意義的殼庭砍。

23:37   Duplicate content roots detected: Path [/Users/system/Work/projectcode/zippkgcode/vx-screen/VScreen] of module [vx-screen.VScreen] was removed from modules [vx-screen.VScreen_App]

gradle 命令時, 默認(rèn)情況下總是會構(gòu)建當(dāng)前目錄下的文件 build.gradle 可以添加-b 參數(shù)-p 參數(shù)

gradle xxxTask -b lib_core.gradle
gradle xxxTask -p 所在目錄 

2. 根項目的構(gòu)建配置

根項目下build.gradle 描述根模塊的項目的插件场晶、屬性、依賴等怠缸。 大家最熟悉的buildscript,里面也一般配置大家熟悉的repositories dependencies 屬性

buildscript {
    ext.gradle_tools_version = '7.0.4' //可定義全局屬性和函數(shù)
    repositories {}
    dependencies {}
}

另外allprojects 的下配置repositories 是不是也熟悉诗轻,這是配置此項目及其每個子項目屬性。因此這里可以很靈活地配置項目所需屬性揭北。如統(tǒng)一編譯配置扳炬、動態(tài)依賴


//配置此項目及其每個子項目。
allprojects { //此方法針對該項目及其子項目執(zhí)行給定的閉包搔体。目標(biāo)Project作為閉包的委托傳遞給閉包恨樟。
    //配置此項目的存儲庫
    repositories {
        google()
        maven { url "https://jitpack.io" } //也可以使用nexus,下文會說到
    }
    configurations.all {  //目前只發(fā)現(xiàn)這里處理依賴相關(guān)的配置 [官網(wǎng)文檔說明](https://docs.gradle.org/current/userguide/resolution_rules.html)
        //每隔24小時檢查遠(yuǎn)程依賴是否存在更新
        resolutionStrategy.cacheChangingModulesFor 24, 'hours'
        //每隔10分鐘..
        //resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
        // 采用動態(tài)版本聲明的依賴緩存10分鐘
        resolutionStrategy.cacheDynamicVersionsFor 10 * 60, 'seconds'

        resolutionStrategy.dependencySubstitution {
            //project&module依賴關(guān)系切換處理 方式1
            substitute(module("cn.mashang.stub_modules:api_box:1.0.0")) using(project(":stub_modules:api_box"))
            substitute(module("cn.mashang.stub_modules:constant_box:1.0.0")) using(project(":stub_modules:constant_box"))
        }
        
        //transitive = false //默認(rèn)為true,一般不會這樣設(shè)疚俱,還可以指定Force劝术、exclude等配置
    }
    //verbose javac: 開啟java 編譯log
    gradle.projectsEvaluated {
        tasks.withType(JavaCompile) {
            options.compilerArgs << "-Xlint" << "-verbose" << "-XprintRounds" << "-XprintProcessorInfo" << "-Xmaxerrs" << "2000"
        }
    }

    //:app 添加在評估此項目后立即調(diào)用的閉包。項目作為參數(shù)傳遞給閉包计螺。當(dāng)屬于該項目的構(gòu)建文件已執(zhí)行時夯尽,此類偵聽器會收到通知。例如登馒,父項目可以將這樣的監(jiān)聽器添加到其子項目匙握。這樣的偵聽器可以在它們的構(gòu)建文件運行后根據(jù)子項目的狀態(tài)進(jìn)一步配置這些子項目。
    project.afterEvaluate { Project p ->
        if (p.plugins.hasPlugin('com.android.application') || p.plugins.hasPlugin('com.android.library')) {
            android {
                compileSdkVersion 32
                defaultConfig {
                    minSdkVersion 21 (默認(rèn))
                    targetSdkVersion 32

                    //構(gòu)建project版本信息,此處能讀取配置后的版本信息
                    if (buildFeatures.buildConfig) {
                        buildConfigField(intType, "BUILD_CODE", "${versionCode}")
                        buildConfigField(str, "BUILD_VERSION_NAME", "\"${versionName}\"")
                    }
                }
                compileOptions {
                    sourceCompatibility = JavaVersion.VERSION_1_8
                    targetCompatibility = JavaVersion.VERSION_1_8
                }
            }
        }
    }
}    

3. 常用公用的構(gòu)建配置

一般我們會定義一些config.gradle和config.properties 配置文件,引用這些文件達(dá)到復(fù)用公用的配置信息陈轿。一般引用方式代碼如下(示例):

apply from: rootProject.file('./buildConfig/baseAndroid.gradle') //這里建議用rootProject.file圈纺,和'./' 避免無法定位文件路徑
//加載properties配置文件
def dict = new Properties()
dict.load(new FileInputStream(rootProject.file("./buildConfig/base.properties")))
def str = dict['DefString']

base.properties 定義了常用的變量值,相對gradle 更方便索引和維護(hù)以及覆蓋屬性 代碼如下(示例):

#要用gbk 編碼
# author rentianlong
#2020年 8月 7日 星期五 12時03分25秒 CST
#java basic type
DefString=String
DefInt=int
DefBool=boolean
DefLong=long
trueStr=true
falseStr=false
#android buildVersion
compileSdkVersion=30
minSdkVersion=19
targetSdkVersion=22
## 項目模塊配置
# 所有模塊app/lib切換開關(guān), 集成相應(yīng)模塊:默認(rèn)true
libModulesIsLib=true

如果是新項目麦射,推薦buildSrc配置信息

baseAndroid.gradle 定義通用的配置, 更方便索引和維護(hù) 代碼如下(示例):

/**
 * 作用描述:
 * Base-Android build file where you can add configuration options common to all sub-projects/modules.
 * Base - Android構(gòu)建文件,您可以添加配置選項常見的所有子項目/模塊蛾娶。
 */

//打印日志
println rootProject.file('./buildConfig/baseAndroid.gradle').getAbsolutePath()
//當(dāng)前模塊信息
def projectDir = getProjectDir()
def projectDirPath = projectDir.absolutePath
println projectDirPath + "\\build.gradle"
def projectName = project.getName()


//Properties工具方法
static def getBool(Properties properties, String key) {
    return Boolean.parseBoolean(properties[key])
}
//加載配置文件
def dict = new Properties()
dict.load(new FileInputStream(rootProject.file("./buildConfig/base.properties")))

def moduleConfig = new File(projectDir, 'debugConfig.properties')
if (moduleConfig.exists()) {
    println 'load submodule_customization configs: ' + moduleConfig.getAbsolutePath()
    dict.load(new FileInputStream(moduleConfig))
}

def BUILD_COMPUTER_TIME = "BUILD_COMPUTER_TIME"
def str = dict['DefString']
def intType = dict['DefInt']
def longType = dict['DefLong']
def trueStr = dict['trueStr']
def compileSdkVersionVar = dict['compileSdkVersion'] as int
def minSdkVersionVar = dict['minSdkVersion'] as int
def targetSdkVersionVar = dict['targetSdkVersion'] as int

//組件化application和library 動態(tài)切換
def hasAppPlugin = pluginManager.hasPlugin("com.android.application")
def libModulesIsLib = getBool(dict, 'libModulesIsLib')

//是否是正式包 (BuildTypes)
boolean isReleaseBuildType() {
    for (String s : gradle.startParameter.taskNames) {
        if (s.contains("Release") | s.contains("release")) {
            return true
        }
    }
    return false
}

def isRelease = isReleaseBuildType()
project.ext.isRelease = isRelease
//println(">>>>> isRelease:$isRelease") //打印日志

//獲取構(gòu)建時間
long getBuildTime() {
    def calendar = Calendar.getInstance()
    if (!isRelease) { //編譯優(yōu)化策略
        calendar.set(Calendar.HOUR_OF_DAY, 0)
        calendar.set(Calendar.MINUTE, 0)
        calendar.set(Calendar.SECOND, 0)
        calendar.set(Calendar.MILLISECOND, 0)
    }
    return calendar.getTimeInMillis()
}

def myBuildTime = "${getBuildTime()}"

if (!hasAppPlugin) { //如果是非app模塊
    if (libModulesIsLib) { //組件化切換調(diào)試常見方案
        plugins.apply("com.android.library")
        println 'apply lib'
    } else {
        hasAppPlugin = true
        plugins.apply("com.android.application")
        println 'apply application'
    }
}
ext.set("hasAppPlugin", hasAppPlugin)
ext.set("libModulesIsLib", libModulesIsLib)

//阿里路由框架啟用, 像UI類庫不需要路由增加編譯壓力
def hasLibARouter = ext.find("lib_arouter") == true
if (hasLibARouter) {
    apply plugin: 'com.alibaba.arouter' //arouter register plugin 實現(xiàn)自動注冊
    println 'apply arouter '
}


android {

    compileSdk compileSdkVersionVar

    //resourcePrefix "submodule_customization_todo" //子模塊定制待辦事項

    defaultConfig {
        multiDexEnabled true
        minSdk minSdkVersionVar
        targetSdk targetSdkVersionVar
        //版本信息默認(rèn)
        versionCode 1
        versionName "1.0.0"
        //資源配置
        resConfigs "en", "zh"

        //ndk配置
        ndk {
            //設(shè)置支持的so庫框架
            abiFilters 'armeabi-v7a'
        }

        //阿里路由框架啟用, 像UI類庫不需要路由增加編譯壓力
        if (hasLibARouter) {
            // 阿里路由框架注解配置, 每個模塊需要依賴
            javaCompileOptions {
                annotationProcessorOptions {
                   arguments = [AROUTER_MODULE_NAME: projectName]
                }
            }
        }

        if (buildFeatures.buildConfig) {
            buildConfigField("boolean", "IS_APPLICATION", "${hasAppPlugin}")
            //構(gòu)建時間
            buildConfigField(longType, BUILD_COMPUTER_TIME, "${myBuildTime}")
            //構(gòu)建project版本信息,此處只能讀取到版本1, 需要放在主腳本android閉包里
            //buildConfigField(intType, "BUILD_CODE", "${versionCode}")
            //buildConfigField(str, "BUILD_VERSION_NAME", "\"${versionName}\"")
        }
    }

    //apk簽名配置
    signingConfigs {
        keystore {
            keyAlias 'xxx'
            keyPassword 'xxx'
            storeFile rootProject.file('./Release/xxx.jks')
            storePassword 'xxx'
            enableV1Signing true
            enableV2Signing true
            //通過 APK v4 簽名,您可以使用 Android 11 中的 ADB 增量 APK 安裝快速部署大型 APK潜秋。此新標(biāo)志負(fù)責(zé)部署過程中的 APK 簽名步驟蛔琅。
            enableV3Signing true
            enableV4Signing true
        }
    }

    buildTypes {
        debug {
            zipAlignEnabled true
            minifyEnabled false
            signingConfig signingConfigs.keystore

            //獨立調(diào)試
            if (!libModulesIsLib) {
                applicationIdSuffix ".debug"
                sourceSets {
                    main { //建立demo資源夾
                        manifest.srcFile 'src/demo/AndroidManifest.xml'
                        java.srcDirs = ['src/main/java', 'src/demo/java']
                        res.srcDirs = ['src/main/res', 'src/demo/res']
                    }
                }
            }
        }
        release {

        }
    }

    //java編譯配置
    compileOptions {
        // Flag to enable support for the new language APIs
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    //lint配置
    lintOptions {
        //不檢查release版本的構(gòu)建
        checkReleaseBuilds false
        //停用 出現(xiàn)錯誤時停止編譯
        abortOnError false
    }
    lintOptions {
        checkDependencies true
    }

    //打包配置
    packagingOptions {
        merge "/arouter/config.properties"
    }

    //dex配置
    dexOptions {
        javaMaxHeapSize "4g"
        //是否支持大工程模式
        jumboMode = true
        //預(yù)編譯
        preDexLibraries = true
        //線程數(shù)
        threadCount = 8
        maxProcessCount = 8 // this is the default value 4 //根據(jù)CPU核心設(shè)置
        //設(shè)置是否啟用dx增量模式 debug時,開啟有加速效果
        incremental true
        //是將 dx 編譯器作為單獨的進(jìn)程運行還是在 Gradle 守護(hù)進(jìn)程 JVM 中運行
        dexInProcess = true
    }
    //adb配置
    adbOptions {
        //timeOutInMs 5 * 1000 //超時
        //installOptions '-r'   //覆蓋
        //installOptions '-r -t' //覆蓋測試 ()
        //installOptions '-t' //測試 ()
        //installOptions '-d' //降級
    }
    buildFeatures {
        //feature enable state config 
    }

    sourceSets {
        main {
        }
    }
}


def autoDependencies = ext.find("auto_dependencies") == false  //自動依懶關(guān)閉 (默認(rèn)開啟)
def autoBasicLibDependencies = ext.find("auto_basiclib_dependencies") == null //自動依懶基本庫開啟 (默認(rèn)開啟)

//公共依賴
dependencies {
    //api fileTree(include: ['*.jar'], dir: 'libs') //確保libs 都是要加入才開啟注釋

    if (autoDependencies) { //自動依懶關(guān)閉
        println("baseAndroid.gradle:auto_dependencies:close " + projectName)
        return null
    }

    rootProject.ext.dependencies.basicApi.each { implementation(it) }
    println("baseAndroid.gradle:basicApi:auto: " + projectName + " <<== " + rootProject.ext.dependencies.basicApi)

    if (autoBasicLibDependencies) {
        //本地lib工程
        rootProject.ext.dependencies.basicLibProject.each {
            String itemName = it
            if (!itemName.contains(projectName)) {
                println("baseAndroid.gradle:basicLibProject:auto: " + projectName + " <<== " + itemName)
                implementation project(itemName)
            }
        }
        //本地lib Nexus
        if ("lib_baseAndroid" != projectName) {
            //lib_baseAndroid 模塊
            //implementation 'cn.mashang.base_modules:lib_baseAndroid:1.0.0'
        }
    }

    //阿里路由框架啟用, 像UI類庫不需要路由增加編譯壓力
    if (hasLibARouter) {
        println("baseAndroid.gradle:lib_arouter:auto: ==> " + projectName)

        implementation('com.alibaba:arouter-api:1.5.2') { // 阿里路由框架api
            exclude group: 'com.android.support', module: 'support-v4'
        }
        annotationProcessor 'com.alibaba:arouter-compiler:1.5.2' // 阿里路由注解框架,每個模塊需要依賴
    }
    //Java 8 及更高版本 API 脫糖支持
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}

以上代碼(示例): 它方便了 lib模塊切換為app、簽名配置峻呛、公共依賴等

  • lib模塊切換為app實現(xiàn)關(guān)鍵:要根據(jù)開關(guān)配置應(yīng)用的application或library插件 關(guān)鍵代碼如下:
if (!hasAppPlugin) { //如果是非app模塊
    if (libModulesIsLib) { //組件化切換調(diào)試常見方案
        plugins.apply("com.android.library") //等同于apply plugin: 'com.android.library'
        println 'apply lib'
    } else {
        hasAppPlugin = true
        plugins.apply("com.android.application") //apply plugin: 'com.android.application'
        println 'apply application'
    }
}

base.properties 下libModulesIsLib 控制所有模塊app/lib切換開關(guān)罗售,false所有l(wèi)ib模塊轉(zhuǎn)換為app

# 所有模塊app/lib切換開關(guān), 集成相應(yīng)模塊:默認(rèn)true
libModulesIsLib=true

工程app簡單示例如下:

apply plugin: 'com.android.application'
apply from: rootProject.file('./buildConfig/baseAndroid.gradle')

模塊lib簡單示例如下:

//默認(rèn)應(yīng)用的是com.android.library
apply from: rootProject.file('./buildConfig/baseAndroid.gradle')

真實項目中辜窑,不需要所有模塊都能單獨調(diào)試和運行所以利用了properties 覆蓋屬性,其實properties是擴(kuò)展Hashtable的寨躁,不難想象load其它配置后相當(dāng)于map.put

def moduleConfig = new File(projectDir, 'debugConfig.properties')
if (moduleConfig.exists()) {
    println 'load submodule_customization configs: ' + moduleConfig.getAbsolutePath()
    dict.load(new FileInputStream(moduleConfig))
}

每個模塊下創(chuàng)建debugConfig.properties 文件, 放置調(diào)試的配置信息
debugConfig.properties ,配置變動記得在View-Gradle 視圖中reload gradle project

# 單模塊app/lib切換開關(guān), 集成相應(yīng)模塊: false或'',null為app, 默認(rèn)true, 修改后需要 reload gradle project
#libModulesIsLib=false

.gitignore 小技巧穆碎,提交完debugConfig.properties 文件,使用它

/build
./debugConfig.properties
  • 公共依賴要注意依賴的合理性和傳遞性

二职恳、nexus與maven-publish

Nexus 最為大家熟知的功能就是 maven 的依賴包管理器所禀。

架設(shè) Nexus 私服有很多優(yōu)點,其中之一就是:

  • 方便上傳團(tuán)隊內(nèi)部的依賴放钦,統(tǒng)一管理色徘,共享

aar 大家最為熟悉,也稱為本地靜態(tài)aar依賴操禀,對比遠(yuǎn)程倉庫中的依賴包 implementation('com.squareup.retrofit2:retrofit:2.4.0') 贺氓,發(fā)現(xiàn)遠(yuǎn)程倉庫中的依賴包的pom.xml文件已經(jīng)包括相應(yīng)okhttp的依賴關(guān)系,這里不展開說明床蜘,所以遠(yuǎn)程依賴包的好處如下:

  • 不用維護(hù)依賴傳遞
  • 代碼不需暴露
  • 加快編譯

1.安裝nexus

官網(wǎng)地址
windows安裝包

運行命令

bin/nexus.exe /run

注冊賬號,nexus相關(guān)配置不一一說明

2.倉庫

這里的倉庫是指項目中依賴的第三方庫蔑水,這個庫所在的位置叫做倉庫邢锯。 在Maven 中,任何一個依賴搀别、插件或者項目構(gòu)建的輸出丹擎,都可以稱之為構(gòu)件。

跟項目下build.gradle 聲明倉庫地址

 ext.maven_local_repo_url = "$projectDir/.repo" //本地倉庫地址
 ext.maven_nexus_snapshots_repo_url = 'http://xx.cpolar.cn/repository/yourProj-snapshots/'
 ext.maven_nexus_releases_repo_url = 'http://xx.cpolar.cn/repository/yourProj-releases/'

repositories 配置

allprojects {
    repositories {
        maven {
            url maven_local_repo_url
        }
        maven {
            url maven_nexus_snapshots_repo_url
            allowInsecureProtocol = true
            credentials {
                username = "guest"
                password = "guest"
            }
        }
        maven {
            url maven_nexus_releases_repo_url
            allowInsecureProtocol = true
            credentials {
                username = "guest" //nexus游客歇父,只允許訪問
                password = "guest"
            }
        }
        google()
        jcenter()
    }

3. maven-publish

Android Gradle 插件 3.6.0 及更高版本支持 Maven Publish Gradle 插件
使用 Maven Publish 插件

多數(shù)模塊都需要發(fā)布蒂培,maven-publish.gradle 示例:

/**
 * 作用描述: maven版本發(fā)布共享庫管理,依賴maven可大大節(jié)省編譯時間
 * 創(chuàng)建人 rentl
 * 創(chuàng)建日期 2022/1/30
 * 修改日期 2022/1/30
 */
println 'Executing maven-publish...'
apply plugin: 'maven-publish'
def ENV = System.getenv()

task generateSourcesJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    classifier 'sources'
}

def groupIdStr = ext.find("groupId")
def artifactIdStr = ext.find("artifactId")
if (groupIdStr == null) {
    System.err.println('Executing maven-publish fail. groupId == null')
    throw new IllegalArgumentException('Executing maven-publish fail. groupId == null')
}
if (artifactIdStr == null) {
    System.err.println('Executing maven-publish fail. artifactId == null')
    throw new IllegalArgumentException('Executing maven-publish fail. artifactId == null')
}

println "Executing maven-publish: groupId=$groupId, artifactId=$artifactId"

afterEvaluate {
    publishing {
        publications {
            release(MavenPublication) {
                from components.release
                groupId = "$groupIdStr"
                artifactId = "$artifactIdStr"
                version = project.android.defaultConfig.versionName
                artifact generateSourcesJar
            }
        }
        repositories {
            maven {
                url = rootProject.ext.maven_local_repo_url
            }
            maven {
                name = "nexus"
                url = project.android.defaultConfig.versionName.endsWith('SNAPSHOT') ? rootProject.ext.maven_nexus_snapshots_repo_url : rootProject.ext.maven_nexus_releases_repo_url
                allowInsecureProtocol = true
                // 倉庫用戶名密碼
                credentials {
                    username = ENV['NEXUS_NAME']
                    password = ENV['NEXUS_PWD']
                }
            }
        }
    }
    def publishTask = project.tasks.getByName('publishReleasePublicationToMavenRepository')
    if (publishTask != null) {
        publishTask.doLast {
            println "maven-publish to .repo, Usage:\nimplementation '${groupIdStr}:${artifactIdStr}:${project.android.defaultConfig.versionName}'"
        }
    }

    def publishTask2 = project.tasks.getByName('publishReleasePublicationToNexusRepository')
    if (publishTask2 != null) {
        publishTask2.doLast {
            println "maven-publish to nexus, Usage:\nimplementation '${groupIdStr}:${artifactIdStr}:${project.android.defaultConfig.versionName}'"
        }
    }
}

模塊的build.gradle 示例:

apply from: rootProject.file('./buildConfig/baseAndroid.gradle')

//maven
ext.groupId = "cn.mashang.base_modules"
ext.artifactId = project.getName()

android {
    resourcePrefix "base_base_"

    defaultConfig {
        versionCode 1
        versionName "1.0.0"
        //versionName "1.0.1-SNAPSHOT"

        consumerProguardFiles "consumer-rules.pro" //lib-proguard


    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

if (!hasAppPlugin) { //這里需要放在這里底下,需要獲取版本信息
    apply from: rootProject.file('./buildConfig/maven-publish.gradle')
}

maven規(guī)約

這里還是要提及一下要遵maven規(guī)約, 大家認(rèn)同的詳細(xì)規(guī)定參考下方:
1)GroupID格式:com.{公司/BU }.業(yè)務(wù)線.[子業(yè)務(wù)線]榜苫,最多4級
正例:com.joymef.platform 或 com.joymef.social.blog
2)ArtifactID格式:產(chǎn)品線名-模塊名护戳。語義不重復(fù)不遺漏,先到倉庫中心去查證一下
3)正例:user-service / user-client / blog-service ) Version
4)開發(fā)階段版本號定義為SNAPSHOT,發(fā)布后版本改為RELEASE(強(qiáng)制)

上面是安卓模塊publish垂睬,java的模塊需要稍微調(diào)整媳荒,maven-jar-publish.gradle示例如下:

/**
 * 作用描述: maven jar版本發(fā)布共享庫管理,依賴maven可大大節(jié)省編譯時間
 * 組件描述:
 * 創(chuàng)建人 rentl
 * 創(chuàng)建日期 2022/2/26
 * 修改日期 2022/2/26
 * 版權(quán) mashang
 */
println 'Executing maven-java-publish...'

compileJava.options.encoding = 'UTF-8'
javadoc.options.encoding = 'UTF-8'
apply plugin: 'maven-publish'
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
    //withJavadocJar()
    withSourcesJar()
}

components.java.withVariantsFromConfiguration(configurations.sourcesElements) {
    skip()
}

def ENV = System.getenv()
def groupIdStr = ext.find("groupId")
def artifactIdStr = ext.find("artifactId")
def versionStr = ext.find("version")
afterEvaluate {
    publishing {
        publications {
            release(MavenPublication) {
                from components.java
                groupId = "$groupIdStr"
                artifactId = "$artifactIdStr"
                version = versionStr
            }
        }
        repositories {
            maven {
                url = rootProject.ext.maven_local_repo_url
            }
            maven {
                name = "nexus"
                url = versionStr.endsWith('SNAPSHOT') ? rootProject.ext.maven_nexus_snapshots_repo_url : rootProject.ext.maven_nexus_releases_repo_url
                allowInsecureProtocol = true
                // 倉庫用戶名密碼
                credentials {
                    username = ENV['NEXUS_NAME']
                    password = ENV['NEXUS_PWD']
                }
            }
        }
    }
    def publishTask = project.tasks.getByName('publishReleasePublicationToMavenRepository')
    if (publishTask != null) {
        publishTask.doLast {
            println "maven-publish to .repo, Usage: implementation '${groupIdStr}:${artifactIdStr}:${versionStr}'"
        }
    }

    def publishTask2 = project.tasks.getByName('publishReleasePublicationToNexusRepository')
    if (publishTask2 != null) {
        publishTask2.doLast {
            println "maven-publish to nexus, Usage: implementation '${groupIdStr}:${artifactIdStr}:${versionStr}'"
        }
    }
}

另外對于gradle task不熟悉的同學(xué)可以打開Gradle 視圖的 不啟用Do not build task list..

注意發(fā)布依賴包時,注意模塊的之間依賴關(guān)系驹饺,模塊盡可能獨立

三钳枕、動態(tài)依賴

  • 模塊下build.gradleadnroid.dependencies{} 中配置當(dāng)前項目的依賴信息,屬于分離式配置的一種
  • 在項目業(yè)務(wù)復(fù)雜的情況下赏壹,業(yè)務(wù)A鱼炒、B模塊依賴關(guān)系大體差不多,對于上面的靜態(tài)依賴蝌借,則不靈活昔瞧,難以復(fù)用指蚁,故想辦法動態(tài)構(gòu)建依賴樹

1.依賴的傳遞性

這里要明白依賴的傳遞性,依賴關(guān)系樹的概念硬爆,示例如下:

C模塊依賴于==> B模塊
B模塊依賴于==> A模塊
由于傳遞性:C模塊同樣依賴于==>A模塊

2.project/module依賴切換

基本實現(xiàn)示例如下:

if(op){
  api project(':base_modules:annotation_lib') //
}else{
  api('cn.mashang.base_modules:annotation_lib:1.0.0-SNAPSHOT') { changing = true }
}

3. 總結(jié)與實踐

初步分析:

  • 模塊的依賴對應(yīng)關(guān)系應(yīng)該采用map 數(shù)據(jù)結(jié)構(gòu)構(gòu)建關(guān)系欣舵,key 為project,value為上面的project/module依賴切換詳情項
  • 依賴的傳遞性可以采用遞歸循環(huán)
  • 壓縮project/module依賴信息為map缀磕, project 缘圈、mouduledep_option 字段必須

進(jìn)步分析:

  • 依賴項配置: implementation袜蚕、api糟把、compileOnlyruntimeOnly牲剃、annotationProcessor遣疯、debugImplementation 等,其中api 具有傳遞性
  • 依賴信息為map凿傅, aar 缠犀、all_dep_optionmy_dep_option 聪舒、description辨液、versiongroup 擴(kuò)展以上字段
  • 建立初步的ext.modules_dependencies=[] key項目PATH, value依賴項等描述依賴關(guān)系箱残,示例如下:
//全局依賴設(shè)置滔迈,只有project依賴或module依賴方式
ext.all_dep_option = "project" //module/project
ext.all_dep_map = [
    //依賴詳情map
    "lib_face_detect"    : [   //project:項目PATH, module:maven, aar:aar文件, all_dep_option:全局參數(shù), my_dep_option:單項參數(shù)
                               "project"       : ":feature_face:lib_face_detect",
                               "module"        : "cn.mashang.feature_face:lib_face_detect:1.0.3",
                               "aar"           : "",
                               "all_dep_option": all_dep_option,
                               //"my_dep_option" : "module", //打開注釋可以單獨生效
                               "description"   : "",
                               "version"       : "",
                               "group"         : "",
    ],
    //添加更多
]           
//添加依賴方式,默認(rèn)`implementation`
ext.addDep = { String score = "implementation", String key ->
    println "addDep: $key"
    Map<String, String> map = new HashMap<>(all_dep_map[key])
    map.put("score", score)
    return map
}
//基礎(chǔ)依賴
def app_core_map = [
            //依賴詳情map
            addDep("lib_face_detect"),
            addDep("api_face")
            addDep("multidex"),
            addDep("lib_comm_ui"),
            addDep("lib_arouter"),
]
    
//項目中依賴關(guān)系被辑,集中管理
ext.modules_dependencies = [
    //key項目PATH, value依賴項
    ":app"  : app_core_map, //復(fù)用A呛贰!盼理!
    ":app1" : app_core_map,//復(fù)用L干健!宏怔!
    ":app2" : app_core_map,//復(fù)用有點吊9戳ā!举哟!
    ":api_face"  : [
        //依賴詳情map
        addDep("commons-net"),
    ]
]  
println "modules_dependencies: " + modules_dependencies         
//添加應(yīng)用依賴工具方法并返回執(zhí)行結(jié)果
ext.utils = [
        applyDependency: { Project p1, Map<String, String> map ->
            def name = p1.name
            def isApplication = p1.pluginManager.hasPlugin("com.android.application")
            def projectInfo = map.getOrDefault("project", "")
            def moduleInfo = map.getOrDefault("module", "")
            def aarInfo = map.getOrDefault("aar", "")
            def all_dep_option = map.getOrDefault("all_dep_option", "")
            def my_dep_option = map.getOrDefault("my_dep_option", "")
            def score = map.getOrDefault("score", "implementation")
            //println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, map: " + map
            if (projectInfo.isBlank() && moduleInfo.isBlank() && aarInfo.isBlank()) {
                System.err.println("warning: projectInfo.isBlank() && moduleInfo.isBlank() && aarInfo.isBlank()")
                return
            }
            boolean applyProject = ("project" == my_dep_option) && !projectInfo.isBlank()
            boolean applyModule = ("module" == my_dep_option) && !moduleInfo.isBlank()
            boolean applyAAR = ("aar" == my_dep_option) && !aarInfo.isBlank()

            if (!(applyProject || applyModule || applyAAR)) {
                applyProject = ("project" == all_dep_option) && !projectInfo.isBlank()
                applyModule = ("module" == all_dep_option) && !moduleInfo.isBlank()
                applyAAR = ("aar" == all_dep_option) && !aarInfo.isBlank()
            }

            if (applyProject) {
                Project depProject = p1.findProject(projectInfo)
                if (depProject == null) {
                    //按需處理思劳,項目沒有include時是否采用遠(yuǎn)程依賴
                    //if (!moduleInfo.isBlank()) {
                    //    p1.dependencies.add(score, moduleInfo, { changing = moduleInfo.contains('cn.mashang') })
                    //    //println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addModule: " + moduleInfo
                    //    return "$score '$moduleInfo'"
                    //}
                    throw new Exception("請檢查 ${projectInfo}")
                }
                p1.dependencies.add(score, depProject)
                //println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addProject: " + projectInfo
                return "$score project('$projectInfo')"
            } else if (applyModule) {
                p1.dependencies.add(score, moduleInfo, { changing = moduleInfo.contains('cn.mashang') })
                //println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addModule: " + moduleInfo
                return "$score '$moduleInfo'"
            } else if (applyAAR) {
                ConfigurableFileCollection depProject = p1.files(aarInfo)
                if (depProject == null) {
                    throw new Exception("請檢查 ${aarInfo}")
                }
                p1.dependencies.add(score, depProject)
                //println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addAar: " + moduleInfo
                return "$score files('$aarInfo')"
            }
            return ""
        }
]
  • 最后應(yīng)用動態(tài)依賴, 配置項目階段可以注入關(guān)系

項目build.gradle

project.afterEvaluate { Project p ->
    //println "> afterEvaluate: " + p
    def list = modules_dependencies.get(p.path)
    if (list == null) {
        return
    }
    def logStr = new StringBuilder(p.name)
    logStr.append(",配置動態(tài)依賴階段 (可參照輸出日志,修改靜態(tài)依賴)")
    logStr.append("\n")
    logStr.append("dependencies {")
    logStr.append("\n")
    //動態(tài)依賴
    list.each { e ->
        def ret = rootProject.ext.utils.applyDependenc
        logStr.append("     ")
        logStr.append(ret)
        logStr.append("\n")
    }
    logStr.append("}\n")
    println logStr
}

Build Project 查看gradle 依賴關(guān)系,歐凱

總結(jié):通過上面分析和示例妨猩,大體實現(xiàn)了項目的動態(tài)依賴潜叛,集中管理,能一鍵切換所有本地模塊和遠(yuǎn)程模塊依賴方式(調(diào)整all_dep_option參數(shù)即可),也能單獨切換某一項(調(diào)整my_dep_option參數(shù)即可)威兜,對于一些特殊模塊也可以聲明aar 參數(shù)強(qiáng)制本地依賴包销斟。更多實踐取決分析項目需要

四、模塊通信

模塊化目的是為了降低低耦合椒舵,提高獨立性蚂踊。集成模塊時,業(yè)務(wù)間需要進(jìn)行相互通信(調(diào)用)笔宿,有經(jīng)驗的同學(xué)會立馬想起路由犁钟、事件、接口方式

1.通信方式

2.路由方式

對于新項目必須引用路由框架如ARouter炬灭、WMRouter醋粟、DRouter,必須用??考慮一番重归,下面簡單介紹一下

  • ARouter 穩(wěn)定米愿、易上手,但使用IProvider 接口的Service是單例鼻吮,且生成代碼增量編譯以及多線程掃描方面一直沒改進(jìn)迭代吗货,不支持SPI是最大的雞肋,作為一款UI路由倒十分合適
  • WMRouter 易調(diào)試狈网,提供了ServiceLoader模塊, 對 AGP7.0 插件Transform不友好,并同ARouter 注解生成多數(shù)代碼用的全是反射
  • DRouter 功能強(qiáng)大笨腥,不僅支持ServiceLoader拓哺,還支持增量編譯,多線程掃描脖母,提升編譯效率士鸥,較難掌握,類似多維過濾器谆级、跨進(jìn)程烤礁、共享內(nèi)存方面不一定需要使用

3.接口方式

  • 微信api化,需要自定義插件肥照,在開始編譯時脚仔,復(fù)制.api文件并重命名.java
  • api lib工程 放入基本數(shù)據(jù)結(jié)構(gòu)和接口 (模塊多時維護(hù)頻繁變)

4.總結(jié)與實踐

從上面分析知,模塊化通信解決方案各有優(yōu)劣舆绎,如路由框架的笨重難以維護(hù)鲤脏,微信api化需要維護(hù) gradle插件,api文件容易被誤修改等,所以要針對自身項目靈活運用猎醇,如下分析一個項目情況:

  • 實踐模塊化項目比較老窥突,已經(jīng)采用了ARouter, 不可能再引用路由或者重寫路由框架來解決SPI支持問題
  • 實踐模塊化項目剛起步,逐步分離20個模塊硫嘶,模塊之間通信較少
  • 項目開啟proguard阻问,如果某些類方法沒有KEEP,會導(dǎo)致反射調(diào)用失敗沦疾,另外項目注重效率称近,盡量避免反射
  • java spiAndroid 上需要讀取整個apk,十分耗時曹鸠,且安卓上是apk煌茬,dex,定義的Service 可能會發(fā)生沖突彻桃,難以定位

通過分析項目情況坛善,需要大概的解決方案如下:

  • 輸出一套簡單實用的android spi機(jī)制. export_table
  • 輸出一套簡單好維護(hù)的 api化 方案,api_box

總結(jié)

以上就是要講的Android-模塊化-項目實踐和探索分享內(nèi)容邻眷,本文僅僅簡單分享了模塊中的靈活運用一些方法眠屎,如有疑問,請和我聯(lián)系肆饶,交流學(xué)習(xí)改衩。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市驯镊,隨后出現(xiàn)的幾起案子葫督,更是在濱河造成了極大的恐慌,老刑警劉巖板惑,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件橄镜,死亡現(xiàn)場離奇詭異,居然都是意外死亡冯乘,警方通過查閱死者的電腦和手機(jī)洽胶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裆馒,“玉大人姊氓,你說我怎么就攤上這事∨绾茫” “怎么了翔横?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長梗搅。 經(jīng)常有香客問我棕孙,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任蟀俊,我火速辦了婚禮钦铺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肢预。我一直安慰自己矛洞,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布烫映。 她就那樣靜靜地躺著沼本,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锭沟。 梳的紋絲不亂的頭發(fā)上抽兆,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機(jī)與錄音族淮,去河邊找鬼辫红。 笑死,一個胖子當(dāng)著我的面吹牛祝辣,可吹牛的內(nèi)容都是我干的贴妻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼蝙斜,長吁一口氣:“原來是場噩夢啊……” “哼名惩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起孕荠,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤娩鹉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后稚伍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弯予,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年槐瑞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阁苞。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡困檩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出那槽,到底是詐尸還是另有隱情悼沿,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布骚灸,位于F島的核電站糟趾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜义郑,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一蝶柿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧非驮,春花似錦交汤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至填大,卻和暖如春戒洼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背允华。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工圈浇, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人例获。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓汉额,卻偏偏與公主長得像,于是被迫代替她去往敵國和親榨汤。 傳聞我的和親對象是個殘疾皇子蠕搜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容