Android-模块化-项目实践和探索分享

@TOC


前言

提示:这里需要提前对Android-模块化-基本知识了解
本文主要分享个人在项目中实现Android模块化中的gradle统一配置、nexus、maven-publish、动态依赖、模块通信等思路


一、gradle统一配置

1. 多模块项目的构建

settings.gradle 是根模块项目以及模块描述文件,include '模块路径(分隔符是冒号)' 或如下别名引入

include 'VScreen_App' //不建议有冒号
project(":VScreen_App").projectDir = file("VScreen") //指定真实模块路径

include 子模块技巧 ,如下

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

//基础组件库
sub_father = ':--base_modules'
include '',
        "$sub_father:lib_arouter", //阿里路由
        "$sub_father:lib_baseAndroid", //安卓基础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",
        ''

业务模块过多,include 业务模块技巧 ,约定在指定目录如下

//业务模块
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}'")
    }
}
//业务模块动态添加 (考虑的业务模块有很多)
def business_modules_dynamically_add = true
if (business_modules_dynamically_add) {
    //动态添加目录底下所有
    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 业务模块 : ${business_modules_symbol}"

老项目工程庞大臃肿,一时无法分离。 一般我们会把这个app工程转化为核心库(下沉给其它工程依赖使用),添加新的壳工程。我们能不能做到不需要空壳app ?答案是肯定的

build.gradle 描述子模块的项目的插件、属性、依赖等。可以在settings.gradle 中自定义脚本文件名

project(":VScreen").buildFileName = "lib_core.gradle"  //改变脚本一个工程打两份工,实测ojbk

Gradle Event Log 提示重复工程,不友好,但是能节省了一个无意义的壳。

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 命令时, 默认情况下总是会构建当前目录下的文件 build.gradle 可以添加-b 参数-p 参数

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

2. 根项目的构建配置

根项目下build.gradle 描述根模块的项目的插件、属性、依赖等。 大家最熟悉的buildscript,里面也一般配置大家熟悉的repositories dependencies 属性

buildscript {
    ext.gradle_tools_version = '7.0.4' //可定义全局属性和函数
    repositories {}
    dependencies {}
}

另外allprojects 的下配置repositories 是不是也熟悉,这是配置此项目及其每个子项目属性。因此这里可以很灵活地配置项目所需属性。如统一编译配置、动态依赖


//配置此项目及其每个子项目。
allprojects { //此方法针对该项目及其子项目执行给定的闭包。目标Project作为闭包的委托传递给闭包。
    //配置此项目的存储库
    repositories {
        google()
        maven { url "https://jitpack.io" } //也可以使用nexus,下文会说到
    }
    configurations.all {  //目前只发现这里处理依赖相关的配置 [官网文档说明](https://docs.gradle.org/current/userguide/resolution_rules.html)
        //每隔24小时检查远程依赖是否存在更新
        resolutionStrategy.cacheChangingModulesFor 24, 'hours'
        //每隔10分钟..
        //resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
        // 采用动态版本声明的依赖缓存10分钟
        resolutionStrategy.cacheDynamicVersionsFor 10 * 60, 'seconds'

        resolutionStrategy.dependencySubstitution {
            //project&module依赖关系切换处理 方式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 //默认为true,一般不会这样设,还可以指定Force、exclude等配置
    }
    //verbose javac: 开启java 编译log
    gradle.projectsEvaluated {
        tasks.withType(JavaCompile) {
            options.compilerArgs << "-Xlint" << "-verbose" << "-XprintRounds" << "-XprintProcessorInfo" << "-Xmaxerrs" << "2000"
        }
    }

    //:app 添加在评估此项目后立即调用的闭包。项目作为参数传递给闭包。当属于该项目的构建文件已执行时,此类侦听器会收到通知。例如,父项目可以将这样的监听器添加到其子项目。这样的侦听器可以在它们的构建文件运行后根据子项目的状态进一步配置这些子项目。
    project.afterEvaluate { Project p ->
        if (p.plugins.hasPlugin('com.android.application') || p.plugins.hasPlugin('com.android.library')) {
            android {
                compileSdkVersion 32
                defaultConfig {
                    minSdkVersion 21 (默认)
                    targetSdkVersion 32

                    //构建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. 常用公用的构建配置

一般我们会定义一些config.gradle和config.properties 配置文件,引用这些文件达到复用公用的配置信息。一般引用方式代码如下(示例):

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 更方便索引和维护以及覆盖属性 代码如下(示例):

#要用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切换开关, 集成相应模块:默认true
libModulesIsLib=true

如果是新项目,推荐buildSrc配置信息

baseAndroid.gradle 定义通用的配置, 更方便索引和维护 代码如下(示例):

/**
 * 作用描述:
 * Base-Android build file where you can add configuration options common to all sub-projects/modules.
 * Base - Android构建文件,您可以添加配置选项常见的所有子项目/模块。
 */

//打印日志
println rootProject.file('./buildConfig/baseAndroid.gradle').getAbsolutePath()
//当前模块信息
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 动态切换
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") //打印日志

//获取构建时间
long getBuildTime() {
    def calendar = Calendar.getInstance()
    if (!isRelease) { //编译优化策略
        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) { //组件化切换调试常见方案
        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 实现自动注册
    println 'apply arouter '
}


android {

    compileSdk compileSdkVersionVar

    //resourcePrefix "submodule_customization_todo" //子模块定制待办事项

    defaultConfig {
        multiDexEnabled true
        minSdk minSdkVersionVar
        targetSdk targetSdkVersionVar
        //版本信息默认
        versionCode 1
        versionName "1.0.0"
        //资源配置
        resConfigs "en", "zh"

        //ndk配置
        ndk {
            //设置支持的so库框架
            abiFilters 'armeabi-v7a'
        }

        //阿里路由框架启用, 像UI类库不需要路由增加编译压力
        if (hasLibARouter) {
            // 阿里路由框架注解配置, 每个模块需要依赖
            javaCompileOptions {
                annotationProcessorOptions {
                   arguments = [AROUTER_MODULE_NAME: projectName]
                }
            }
        }

        if (buildFeatures.buildConfig) {
            buildConfigField("boolean", "IS_APPLICATION", "${hasAppPlugin}")
            //构建时间
            buildConfigField(longType, BUILD_COMPUTER_TIME, "${myBuildTime}")
            //构建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。此新标志负责部署过程中的 APK 签名步骤。
            enableV3Signing true
            enableV4Signing true
        }
    }

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

            //独立调试
            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版本的构建
        checkReleaseBuilds false
        //停用 出现错误时停止编译
        abortOnError false
    }
    lintOptions {
        checkDependencies true
    }

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

    //dex配置
    dexOptions {
        javaMaxHeapSize "4g"
        //是否支持大工程模式
        jumboMode = true
        //预编译
        preDexLibraries = true
        //线程数
        threadCount = 8
        maxProcessCount = 8 // this is the default value 4 //根据CPU核心设置
        //设置是否启用dx增量模式 debug时,开启有加速效果
        incremental true
        //是将 dx 编译器作为单独的进程运行还是在 Gradle 守护进程 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  //自动依懒关闭 (默认开启)
def autoBasicLibDependencies = ext.find("auto_basiclib_dependencies") == null //自动依懒基本库开启 (默认开启)

//公共依赖
dependencies {
    //api fileTree(include: ['*.jar'], dir: 'libs') //确保libs 都是要加入才开启注释

    if (autoDependencies) { //自动依懒关闭
        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实现关键:要根据开关配置应用的application或library插件 关键代码如下:
if (!hasAppPlugin) { //如果是非app模块
    if (libModulesIsLib) { //组件化切换调试常见方案
        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切换开关,false所有lib模块转换为app

# 所有模块app/lib切换开关, 集成相应模块:默认true
libModulesIsLib=true

工程app简单示例如下:

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

模块lib简单示例如下:

//默认应用的是com.android.library
apply from: rootProject.file('./buildConfig/baseAndroid.gradle')

真实项目中,不需要所有模块都能单独调试和运行所以利用了properties 覆盖属性,其实properties是扩展Hashtable的,不难想象load其它配置后相当于map.put

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

每个模块下创建debugConfig.properties 文件, 放置调试的配置信息
debugConfig.properties ,配置变动记得在View-Gradle 视图中reload gradle project

# 单模块app/lib切换开关, 集成相应模块: false或'',null为app, 默认true, 修改后需要 reload gradle project
#libModulesIsLib=false

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

/build
./debugConfig.properties
  • 公共依赖要注意依赖的合理性和传递性

二、nexus与maven-publish

Nexus 最为大家熟知的功能就是 maven 的依赖包管理器。

架设 Nexus 私服有很多优点,其中之一就是:

  • 方便上传团队内部的依赖,统一管理,共享

aar 大家最为熟悉,也称为本地静态aar依赖,对比远程仓库中的依赖包 implementation('com.squareup.retrofit2:retrofit:2.4.0') ,发现远程仓库中的依赖包的pom.xml文件已经包括相应okhttp的依赖关系,这里不展开说明,所以远程依赖包的好处如下:

  • 不用维护依赖传递
  • 代码不需暴露
  • 加快编译

1.安装nexus

官网地址
windows安装包

运行命令

bin/nexus.exe /run

注册账号,nexus相关配置不一一说明

2.仓库

这里的仓库是指项目中依赖的第三方库,这个库所在的位置叫做仓库。 在Maven 中,任何一个依赖、插件或者项目构建的输出,都可以称之为构件。

跟项目下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 插件

多数模块都需要发布,maven-publish.gradle 示例:

/**
 * 作用描述: maven版本发布共享库管理,依赖maven可大大节省编译时间
 * 创建人 rentl
 * 创建日期 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规约

这里还是要提及一下要遵maven规约, 大家认同的详细规定参考下方:
1)GroupID格式:com.{公司/BU }.业务线.[子业务线],最多4级
正例:com.joymef.platform 或 com.joymef.social.blog
2)ArtifactID格式:产品线名-模块名。语义不重复不遗漏,先到仓库中心去查证一下
3)正例:user-service / user-client / blog-service ) Version
4)开发阶段版本号定义为SNAPSHOT,发布后版本改为RELEASE(强制)

上面是安卓模块publish,java的模块需要稍微调整maven-jar-publish.gradle示例如下:

/**
 * 作用描述: maven jar版本发布共享库管理,依赖maven可大大节省编译时间
 * 组件描述:
 * 创建人 rentl
 * 创建日期 2022/2/26
 * 修改日期 2022/2/26
 * 版权 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不熟悉的同学可以打开Gradle 视图的 不启用Do not build task list..

注意发布依赖包时,注意模块的之间依赖关系,模块尽可能独立

三、动态依赖

  • 模块下build.gradleadnroid.dependencies{} 中配置当前项目的依赖信息,属于分离式配置的一种
  • 在项目业务复杂的情况下,业务A、B模块依赖关系大体差不多,对于上面的静态依赖,则不灵活,难以复用,故想办法动态构建依赖树

1.依赖的传递性

这里要明白依赖的传递性,依赖关系树的概念,示例如下:

C模块依赖于==> B模块
B模块依赖于==> A模块
由于传递性:C模块同样依赖于==>A模块

2.project/module依赖切换

基本实现示例如下:

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

3. 总结与实践

初步分析:

  • 模块的依赖对应关系应该采用map 数据结构构建关系,key 为project,value为上面的project/module依赖切换详情项
  • 依赖的传递性可以采用递归循环
  • 压缩project/module依赖信息为mapprojectmouduledep_option 字段必须

进步分析:

  • 依赖项配置: implementationapicompileOnlyruntimeOnlyannotationProcessordebugImplementation 等,其中api 具有传递性
  • 依赖信息为mapaarall_dep_optionmy_dep_optiondescriptionversiongroup 扩展以上字段
  • 建立初步的ext.modules_dependencies=[] key项目PATH, value依赖项等描述依赖关系,示例如下:
//全局依赖设置,只有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:全局参数, my_dep_option:单项参数
                               "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"         : "",
    ],
    //添加更多
]           
//添加依赖方式,默认`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
}
//基础依赖
def app_core_map = [
            //依赖详情map
            addDep("lib_face_detect"),
            addDep("api_face")
            addDep("multidex"),
            addDep("lib_comm_ui"),
            addDep("lib_arouter"),
]
    
//项目中依赖关系,集中管理
ext.modules_dependencies = [
    //key项目PATH, value依赖项
    ":app"  : app_core_map, //复用!!!
    ":app1" : app_core_map,//复用!!!
    ":app2" : app_core_map,//复用有点吊!!!
    ":api_face"  : [
        //依赖详情map
        addDep("commons-net"),
    ]
]  
println "modules_dependencies: " + modules_dependencies         
//添加应用依赖工具方法并返回执行结果
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时是否采用远程依赖
                    //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 ""
        }
]
  • 最后应用动态依赖, 配置项目阶段可以注入关系

项目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(",配置动态依赖阶段 (可参照输出日志,修改静态依赖)")
    logStr.append("\n")
    logStr.append("dependencies {")
    logStr.append("\n")
    //动态依赖
    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 依赖关系,欧凯

总结:通过上面分析和示例,大体实现了项目的动态依赖,集中管理,能一键切换所有本地模块和远程模块依赖方式(调整all_dep_option参数即可),也能单独切换某一项(调整my_dep_option参数即可),对于一些特殊模块也可以声明aar 参数强制本地依赖包。更多实践取决分析项目需要

四、模块通信

模块化目的是为了降低低耦合,提高独立性。集成模块时,业务间需要进行相互通信(调用),有经验的同学会立马想起路由、事件、接口方式

1.通信方式

2.路由方式

对于新项目必须引用路由框架如ARouter、WMRouter、DRouter,必须用💜考虑一番,下面简单介绍一下

  • ARouter 稳定、易上手,但使用IProvider 接口的Service是单例,且生成代码增量编译以及多线程扫描方面一直没改进迭代,不支持SPI是最大的鸡肋,作为一款UI路由倒十分合适
  • WMRouter 易调试,提供了ServiceLoader模块, 对 AGP7.0 插件Transform不友好,并同ARouter 注解生成多数代码用的全是反射
  • DRouter 功能强大,不仅支持ServiceLoader,还支持增量编译,多线程扫描,提升编译效率,较难掌握,类似多维过滤器、跨进程、共享内存方面不一定需要使用

3.接口方式

  • 微信api化,需要自定义插件,在开始编译时,复制.api文件并重命名.java
  • api lib工程 放入基本数据结构和接口 (模块多时维护频繁变)

4.总结与实践

从上面分析知,模块化通信解决方案各有优劣,如路由框架的笨重难以维护,微信api化需要维护 gradle插件,api文件容易被误修改等,所以要针对自身项目灵活运用,如下分析一个项目情况:

  • 实践模块化项目比较老,已经采用了ARouter, 不可能再引用路由或者重写路由框架来解决SPI支持问题
  • 实践模块化项目刚起步,逐步分离20个模块,模块之间通信较少
  • 项目开启proguard,如果某些类方法没有KEEP,会导致反射调用失败,另外项目注重效率,尽量避免反射
  • java spiAndroid 上需要读取整个apk,十分耗时,且安卓上是apk,dex,定义的Service 可能会发生冲突,难以定位

通过分析项目情况,需要大概的解决方案如下:

  • 输出一套简单实用的android spi机制. export_table
  • 输出一套简单好维护的 api化 方案,api_box

总结

以上就是要讲的Android-模块化-项目实践和探索分享内容,本文仅仅简单分享了模块中的灵活运用一些方法,如有疑问,请和我联系,交流学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容