什么是组件化?

在项目开发中往往根据功能的不同划分多个模块,在小型项目中大家往往会把各模块放置到对应的包当中,这样是很方便,小编也喜欢这样干。但是,划重点!是“小”项目中,要是大项目还是按包来划分后期改动的时候会不会牵一发而动全身呢?会的。所以组件化登场了,将每个功能分成独立模块,模块之间独立性强却有些许联系。如此,我们在更改代码时就不会遇到前面所说的困扰了。下面,我上一份图来帮助大家理解。

image-20221212180059339

  • app壳工程
    • 模块之间相互独立,那就需要一个“主子”来统筹全局,那就是app壳工程
  • 业务组件化
    • 这就是各个功能模块,比如用户注册登录、首页、个人等等
  • 公共基础库
    • 这里集成着项目中每个或多个模块需要的基础功能,也就是共享库。比如基本数据、网络访问框架、Application(的context)、工具类Utils。

组件化优势

  • 层次分明,即使是另一个人接手项目也能清晰项目框架,很快入手项目
  • 拆卸方便,某个模块不想要了或者要替换成另一个模块,直接拆卸即可,只需要做很小的改动
  • 不相互依赖,前面提到过,依赖性太强会导致改动太大
  • 重复利用

集成环境/组件环境部署配置

创建新模块

在Android Studio中创建一个项目,在左上角app处右键new一个module出来,见下图,这就将作为业务组件层。当然,一个项目中的业务组件层有很多module,再new就好

image-20221212181551729

创建公共基础库

几乎同样的步骤和注意事项,但是请注意:这里创建的是Android Library依赖库,也就是没有UI的代码仓库。

并且,公共依赖库最好只有这一个。

image-20221212181931950

见下图可以看到,小编创建了两个业务组件以及一个公共依赖库。

image-20221212182206688

环境部署

这个要麻烦一点,主要是细心。我们先来看看项目中gradle的组成

1、这是gradle的根文件,咱们用不到

2、这是整个项目的build.gradle文件,后面我将在这做一点操作,相信大家也看出来,就那一行代码,后面再说。

image-20221212182727574

3、这是每个模块的build.gradle,这里展示app的,app壳咱也能当做一个模块,只是它是“老大哥”,和每个组件有联系。

image-20221212182906438

下面,我们来部署环境。

为什么要环境部署呢?下图大家都熟悉,是build.gradle里的配置,我们需要保证各模块的版本配置相同,才能保证在相同的环境运行。

观察下面的这些变量都有一个共同点——>以键值对的形式存在,所以我们要想办法造一个公共库,直接调取里面的变量。

image-20221212195515443

  1. new一个gradle扩展块,这是groovy语法,不过对于学过面向对象的大家也不是问题;

    image-20221212183111198

    //扩展块
    ext{

    //区分测试环境和正式环境
    isRelease = true

    //模拟正式环境和测试环境的服务器地址
    url = [
    "debug" :"https://192.188.22.99/debug",
    "release" :"https://192.188.22.99/release"
    ]

    //建立Map存储,key和value 都是自定义的
    androidID = [
    compileSdk :32,
    applicationId :"com.example.skindemo",
    minSdk :28,
    targetSdk :32,
    versionCode :1,
    versionName :"1.0",
    testInstrumentationRunner :"androidx.test.runner.AndroidJUnitRunner"
    ]

    appID = [
    app : "com.example.skindemo",
    home: "com.example.home",
    login: "com.example.login"
    ]

    dependenciesID = [
    "core-ktx" :'androidx.core:core-ktx:1.7.0',
    "appcompat" :'androidx.appcompat:appcompat:1.3.0',
    "material" :'com.google.android.material:material:1.4.0',
    implementation :'androidx.constraintlayout:constraintlayout:2.0.4',
    "navigation-fragment-ktx" :'androidx.navigation:navigation-fragment-ktx:2.3.5',
    "navigation-ui-ktx" :'androidx.navigation:navigation-ui-ktx:2.3.5'
    ]

    }
    • isRelease:用来判断是否是正式环境,如果是测试环境,那么功能子模块就能独立运行,否则就只能寄存与app壳运行。什么意思呢?看到下方的叉叉没有,那就是不能独立运行,也就是我们把isRelease设置为true的情况。

      image-20221212194830481

    • url:这个不用管,是模拟的两种服务器地址,这是虚拟的,没有用,读者可不敲。

    • androidID:这是个键值对存储,就和Map一样,里面存储的value信息就是前面提到的版本信息。

    • appID:不同模块的路径

    • dependenciesID:看名字就知道是模块中各种依赖的键值对,但注意只有implementation相关的才提到扩展gralde中

      image-20221212195722179

  2. 整个项目的build.gradle(注意不是root.gradle)中引入我们刚创建的扩展块,这样就能在模块的build.gradle访问扩展块里的数据了。

    image-20221212195937100

  3. 修改模块的build.gradle的代码,我以home为例。

    注意:最开始的isRelease这部分判断是业务组件(功能模块)的独有部分,前面已经对这个字段作用解说过了。

    def androidID = rootProject.ext.androidID这段代码不要也行,只不过有了这段代码相当于持有缓存,下次访问就直接访问缓存,相当于一种优化。

    if (isRelease){
    apply plugin: 'com.android.library'//正式环境 不能独立运行
    }else{
    apply plugin: 'com.android.application'//测试环境
    }
    apply plugin: 'org.jetbrains.kotlin.android'

    def androidID = rootProject.ext.androidID
    android {
    compileSdk androidID.compileSdk

    defaultConfig {
    if (!isRelease){
    applicationId appID.home
    }
    minSdk androidID.minSdk
    targetSdk androidID.targetSdk
    versionCode androidID.versionCode
    versionName androidID.versionName

    testInstrumentationRunner androidID.testInstrumentationRunner
    viewBinding{
    enabled = true
    }
    }

    这里的代码看着是不是很舒服?因为groovy语法属于Java子语言的一种,所以看着非常好理解,这里用到了foreach。另外,implementation project(':common')这是依赖其他模块的操作,common是所有模块的公共依赖库,所以都会对其依赖。依赖后,就能使用里面的类和方法。

    dependencies {

    dependenciesID.each{k,v -> implementation v}

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    implementation project(':common') //公共基础库
    }

    再展现一下common的build.gradle,因为其不同的多一点。(plugins和dependencies处的不同)

    plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
    }

    def androidID = rootProject.ext.androidID
    android {
    compileSdk androidID.compileSdk

    defaultConfig {
    minSdk androidID.minSdk
    targetSdk androidID.targetSdk

    testInstrumentationRunner androidID.testInstrumentationRunner
    consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    }
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
    jvmTarget = '1.8'
    }
    }

    dependencies {

    // implementation 'androidx.core:core-ktx:1.7.0'
    // implementation 'androidx.appcompat:appcompat:1.3.0'
    // implementation 'com.google.android.material:material:1.4.0'
    dependenciesID.each{k,v ->
    if (k=="core-ktx"){
    implementation v
    }
    if (k=="appcompat"){
    implementation v
    }
    if (k=="material"){
    implementation v
    }
    }
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    }

    app壳作为“老大”,则需要对所有模块进行依赖。

    dependencies {

    // implementation 'androidx.core:core-ktx:1.7.0'
    // implementation 'androidx.appcompat:appcompat:1.3.0'
    // implementation 'com.google.android.material:material:1.4.0'
    // implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    // implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
    // implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'

    dependenciesID.each{k,v -> implementation v}

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'


    implementation project(':common')

    if (isRelease){
    //依附于app壳
    implementation project(':login')
    implementation project(':home')
    }else {
    //不能依附app壳,因为login home都能独立运行,依附不了,否则报错
    }
    }

组件化中子模块交互方式

类加载的方式

val targetClass = Class.forName("com.example.login.LoginActivity")
val intent = Intent(this,targetClass).apply {
putExtra("name","joker")
}
startActivity(intent)

为什么这种方式可以呢?当我们buildAPK的时候可以发现,这工程中创建的所有的类都在一个文件夹下。

全局Map

image-20221213182001925

data class PathBean(var path: String?, var clazz: Class<*>?)
object RecordPathManager {
private val maps: MutableMap<String, MutableList<PathBean>> = HashMap()

/**
* @param groupName 组名
* @param pathName 路径名
* @param clazz 跳转目标class对象
*/
fun addGroupInfo(groupName: String, pathName: String?, clazz: Class<*>?) {
var list = maps[groupName]
if (null == list) {
list = ArrayList()
}
list.add(PathBean(pathName, clazz))
//存入仓库
maps[groupName] = list
}

fun startTargetActivity(groupName: String, pathName: String): Class<*>? {
val list = maps[groupName]
?: return null
//遍历寻找“PathBean”对象
for (pathBean in list) {
if (pathName.equals(pathBean.path, ignoreCase = true)) {
return pathBean.clazz
}
}
return null
}
}

然后需要在app壳的Application中注册Activity:

override fun onCreate() {
super.onCreate()
//在注册表中注册Activity
mContext = context
RecordPathManager.addGroupInfo("app","MainActivity",MainActivity::class.java)
RecordPathManager.addGroupInfo("home","HomeActivity",HomeActivity::class.java)
RecordPathManager.addGroupInfo("login","LoginActivity",LoginActivity::class.java)
}

使用1:

val clazz = RecordPathManager.startTargetActivity("home","HomeActivity")
Intent(this,clazz).apply {
startActivity(this)
}

使用2:

也就是添加了携带数据和跳转动画

val clazz = RecordPathManager.startTargetActivity("home","HomeActivity")
val intent = Intent(this,clazz).apply {
putExtra("data","kotlin")
putExtra("second_data",200)
}
binding.moveToHome.setOnClickListener {
startActivity(intent)
overridePendingTransition(R.anim.translate_in,R.anim.translate_out)
}

ARouter

使用阿里的开源框架,详情可见:https://github.com/alibaba/ARouter

我遇到的一些坑

运行时产生多个“软件”

运行后手机桌面怎么有其他模块的图标?

对各个功能模块的AndroidManifest.xml进行如下操作:

将我注释的部分注释或删除,开发工具会自动将AndroidManifest.xml合并,一个App只能有一个自启动的Activity。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.home">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SkinDemo">
<activity
android:name=".HomeActivity"
android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->

<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
</activity>
</application>

</manifest>

跳转后还是在原来的activity中

将不同模块的activity改名即可,因为创建新模块时系统默认是main_activity

关于Application

我在前面的开发技巧中提过在Application中初始化全局的context,但是在组件化中却出了点问题,最终只有一个Application会生效,但是我们又不能让app壳被其他模块依赖,否则将出现循环依赖的问题。嗯?!公共依赖库中的代码不是其他模块都可以访问嘛,可以直接只创建公共基础库的Application吗?可以,但是如果使用前面的Map方式跳转怎么办,公共基础库没有依赖其他模块,肯定注册不了啊。那么怎么办,可以让公共依赖库创建一个BaseApplication,创建context,然后让app的Application继承BaseApplication,再初始化context。

BaseApplication

open class BaseApplication: Application() {
companion object{
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}

override fun onCreate() {
super.onCreate()
context = applicationContext
}
}

app的Application

class SkinApplication: BaseApplication() {
companion object{
@SuppressLint("StaticFieldLeak")
lateinit var mContext: Context
}
override fun onCreate() {
super.onCreate()
//在注册表中注册Activity
mContext = context
RecordPathManager.addGroupInfo("app","MainActivity",MainActivity::class.java)
RecordPathManager.addGroupInfo("home","HomeActivity",HomeActivity::class.java)
RecordPathManager.addGroupInfo("login","LoginActivity",LoginActivity::class.java)
}
}

小提醒:别忘了在AndroidManifest.xml中注册app的Application。

小扩展

可不可以通过Java/kotlin代码层改变gradle定义的变量值呢?就比如我不想切到gradle而想改变isRelease的值,以达到改变项目是否发布的状态。

漏普吧不冷啊。

且看下面的操作:

这是我在app的gradle中添加的一行代码,其他模块也可以。

image-20221214185156341

看到没有,我能在MainActivity中获取isRelease了

image-20221214185349505

这种应用还有很多,就比如前面小提了一下关于正式发布与测试的服务器地址的切换也可以这样操作。

image-20221214185638066

看见没有,很简单。

总结

之前在简书中作的那篇组件化文章太乱了,当时小编自己都没捋清楚前因后果。好在小编没有放弃,现在发现很简单嘛!于是就作了这篇文章,前后逻辑很清晰。