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

2023-10-26


前言

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


组件化项目demo

一、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 ?答案是肯定的

TODO:补图

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,必须用

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android-模块化-项目实践和探索分享 的相关文章

随机推荐

  • linux安装管理

    今日学习总结 Linux安装管理 RPM Package Manager RPM是一个强大的命令行驱动的软件包管理工具 用来安装 卸载 校验 查询和更新 Linux 系统上的 软件包 RPM软件的安装 删除 更新只有root权限才能使用 对
  • kafka 3.5 生产者请求中的acks,在服务端如何处理源码

    一 生产者客户端配置参数acks说明 1 acks 1 2 acks 0 3 acks 1 二 请求在写入Leader的数据管道之前 则会验证Leader的ISR副本数量和配置中的最小ISR数量 1 Leader的ISR小于配置文件中min
  • [FAQ14812]如何快速对系统重启问题进行归类

    DESCRIPTION 当手机发生系统重启 即导致kernel重启的异常时 会在手机中的 data aee exp目录下保存异常重启的db 工程师可以通过GAT的bug report功能 或者直接通过adb pull 把对应的db从手机中抓
  • C/C++程序的内存开辟

    C C 程序内存分配的几个区域 1 栈区 stack 在执行函数时 函数内局部变量的存储单元都可以在栈上创建 函数执行结 束时这些存储单元自动被释放 栈内存分配运算内置于处理器的指令集中 效率很高 但是 分配的内存容量有限 栈区主要存放运行
  • 放弃几百万年薪的后续

    厂长 和洋哥认识很久了 最近他从网易离职 放弃了几百万的年薪 全身心的投入AIGC 刚开始我得到这个消息很是诧异 在详谈之后才明白了洋哥背后的思考逻辑 刚好今天他也写了篇文章做了解释 我申请转载过来给大家分享一下 洋哥 网名findyi 前
  • c语言 m个数 取n个数,本题要求编写程序,根据公式Cnm=m!(n−m)!n!算出从n个不同元素中取出m个元素(m≤n)的组合数。...

    实验2 4 7 求组合数 15分 本题要求编写程序 根据公式C n m m n m n 算出从n个不同元素中取出m个元素 m n 的组合数 建议定义和调用函数fact n 计算n 其中n的类型是int 函数类型是double 输入格式 输入
  • 推荐两款在线免费的可视化网页编辑器:Layoutit!和RXStudio

    2023年8月22日 周二上午 今天看Boost库的html文档时 突然也想自己写一个这样的html文档 但又不想手敲代码 于是在网上找到了很多可视化的网页编辑器 最后我觉得这两款编辑器比较好用 目录 Layout 官网 使用体验 使用方法
  • phpcms V9 局域网访问问题

    按下列步骤操作修改域名 1 打开caches configs system php文件 然后批量替换里面的域名 如把http 127 0 0 1替换为您的新域名 如http 192 168 114 140 保存 2 登陆后台 找到 设置 站
  • anaconda安装tensorboard过慢

    不要从官网下载tensorboard conda activate your environment name 通过国内的豆瓣源下载 pip install i https pypi douban com simple tensorboar
  • React 之 解决页面多次重复渲染造成页面卡顿问题

    一 场景案例 描述 画面上存在多个卡片 卡片是动态渲染的 每个卡片有自己的相关操作 比如 点击卡片的菜单项 出现弹框 弹框中需要填入相关的信息 在输入信息的过程中会出现一个字一个字出来的卡顿效果 或者是点击卡片菜单项 弹框出现的时候比较卡顿
  • 编译原理:LL(1)、LR(0)、SLR(1)分析(大招)

    LL 1 含义 第一个L代表从左到右扫描输入序列 第二个L表示产生最左推导 1表示在确定分析器的每一步动作时向前看一个终结符 判断 第一步找到能够推出是空的非终结符 像在这个文法中 画勾的就是可以推出是空的非终结符 第二步 求first集合
  • 软件工程之总体设计

    总体设计是软件工程中的一个重要阶段 它关注整个系统的结构和组织 旨在将系统需求转化为可执行的软件解决方案 总体设计决定了系统的架构 模块划分 功能组织以及数据流和控制流等关键方面 可行性研究 具体方面 经济可行性 技术可行性 操作可行性 法
  • 2020 JAVA eclipse 中文汉化包 安装教程--傻瓜式操作

    下载的是 Eclipse IDE for Enterprise Java Developers includes Incubating components 这个版本 直接下载了 解压就可以使用 用的是官方的汉化包 下载 解压 复制 粘贴
  • 5个步骤实现软件质量的快速提升

    每个人都希望软件质量更高 速度更快 对现代软件开发团队的要求是巨大的 从竞争和市场压力的增加 功能和复杂性的增加 到对产品质量 安全性和可靠性的更高期望 敏捷开发方法常常受到追捧 因为它能更快地响应变化 更好地实现客户需求 但是 敏捷和De
  • ESXI虚拟机厚置备延迟置零转换为Thin Provision方法

    最近有博友提出一个需求 他们公司的服务器磁盘空间不足了 现在无法正常创建虚拟机 其实并没有使用到这么多空间 只是因为划了这么多空间给虚拟机 所以造成磁盘空间不足 那么是否有什么解决的方法了 详细了解发现虚拟机在配置磁盘的时候设置的是厚置备延
  • RC低通滤波器

    先来几个不错的资源链接 1 RC滤波器截止频率在线计算器 http www eechina com tools rc filter cutoff frequency html 2 详谈一阶RC低通滤波器如何过滤高频噪声 网上不错的一个帖子
  • Linux学习-43-挂载Linux系统外的文件mount和卸载文件系统umount命令用法

    10 10 mount命令详解 挂载Linux系统外的文件 所有的硬件设备必须挂载之后才能使用 新硬盘先格式化后创建分区 再对分区进行挂载 只不过 有些硬件设备 比如硬盘分区 在每次系统启动时会自动挂载 而有些 比如 U 盘 光盘 则需要手
  • 使用w,vmstat命令,top命令,sar命令,nload命令

    监控系统状态 w命令 uptime load average 0 00 0 01 0 05 上面这条显示的就是系统负载 后面有三段数字 root localhost w 21 33 04 up 41 min 1 user load aver
  • STS & 开发异常

    1 Failed to start component 情景 本地 tomcat 部署了两个项目 一个provider 一个 server 前台通过server访问 provider 在开发的时候 将tomcat部署的服务 Clean 或者
  • Android-模块化-项目实践和探索分享

    文章目录 前言 一 gradle统一配置 1 多模块项目的构建 2 根项目的构建配置 3 常用公用的构建配置 二 nexus与maven publish 1 安装nexus 2 仓库 3 maven publish 三 动态依赖 1 依赖的