什么是组件化

组件化的工作方式信奉独立、完整、自由组合。目标就是尽可能把设计与开发中的元素独立化,使它具备完整的局部功能,通过自由组合来构成整个产品。将每个业务模块分成单独的组件,可单独打包、测试,这种方式能够让我们的项目具有更高的可维护性和可读性。

为什么需要组件化

我们在一些中大型的项目中可以看到,他们少则几个,多则几十个,甚至上百个组件,为什么这样做呢?在早起的项目中,都是单一的模块,进行业务分包的模式开发的,这样随着项目增大,项目失去层次感,维护起来越来越棘手。再一个就是耦合度太高,稍不注意就有不同业务模块的相互调用。组件化的出现,正好可以解决这些问题。

组件化项目结构

Android模块化和组件 android组件化方案_android

这种架构下,主工程就是个空壳子,所有业务模块之间平起平坐,不再相互依赖,如果将来要砍掉某一个模块,可以直接去掉此模块的依赖,省去了大量的无用工作。

组件化项目的实现

组件化项目具体怎么实现呢,这里我们一步一步来操作,假设我们项目中需要一个商品模块和一个订单模块:

  • 这两个模块都依赖BaseLibrary
  • app作为一个壳工程,依赖 goods和order两个业务模块

Android模块化和组件 android组件化方案_android_02

一、 gradle优化

我曾经的项目中gradle 都是一团糟,甚至每个组件的sdk版本号都不一样:

Android模块化和组件 android组件化方案_组件化_03

现在我们要做的,就是把每个模块的版本号进行统一管理,当升级版本时,改一处即可。

首先需要搞清楚gradle的执行流程

Android模块化和组件 android组件化方案_Android模块化和组件_04

拿这个项目举例:在主工程下面有一个settings.gradle和一个build.gradle,每个模块下面都有一个build.gradle。

  • 在项目构建时,会先执行主目录下的settings.gradle,用与标记主工程和模块,执行完了这一步我们才会看到项目下每个模块名都加粗显示了。
  • 执行完了settings.gradle, 就会执行主工程下的build.gradle,在这里可以配置一些所有子模块都用到的功能,比如引入仓库,插件依赖等。
  • 最后才会执行每个模块下面的build.gradle,完成对项目的构建。

1. 配置文件

那我们要实现gradle的优化,就可以考虑创建一个配置文件,在主工程下的build.gradle引入,这样每个子模块的gradle都可以使用了

// config.gradle

ext {
    // android开发版本配置
    android = [
            compileSdkVersion: 30,
            buildToolsVersion: "30.0.2",
            applicationId    : "cn.com.itink.newenergy",
            minSdkVersion    : 21,
            targetSdkVersion : 30,
            versionCode      : 1,
            versionName      : "1.0.0",
    ]

    // BaseLibrary基础库
    dep_base = [
            // 基础库
            "kotlinstd"          : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version",
            "anko"               : "org.jetbrains.anko:anko-commons:0.10.8",
            "corektx"            : "androidx.core:core-ktx:1.3.2",
            "appcompat"          : "androidx.appcompat:appcompat:1.2.0",
            "constraintlayout"   : "androidx.constraintlayout:constraintlayout:2.0.4",
            "recyclerview"       : "androidx.recyclerview:recyclerview:1.1.0",
            "material"           : "com.google.android.material:material:1.2.1",
            "multidex"           : "androidx.multidex:multidex:2.0.1",
    ]
}

在以上代码中,android和dep_base作为变量,他们的值都是map集合。 android用于统一管理版本号,dep_base用于统一管理base依赖项。注:可以根据自己项目的需要进行配置。最后别忘了在build.gradle中应用

Android模块化和组件 android组件化方案_ARouter_05

进行以上操作之后,我们就可以统一管理每个模块的版本号了:

Android模块化和组件 android组件化方案_ARouter_06

2. 依赖管理

现在要对base进行统一的依赖管理,首先可以删除所有module的依赖,并引入Base模块:

dependencies {
    implementation project(":BaseLibrary")
}

在base模块中引入所有module需要的依赖,使用api方法引入,可以进行依赖穿透:

dependencies {
    rootProject.ext.dep_base.each {
        api it.value
    }
}

这里简单提一下, rootProject.ext.dep_base就是我们定义的dep_base变量, each就是遍历,it.value就是map集合中每一项的值。这样就完成了所有的依赖

3. 开发环境/集成环境

上面我们提到,业务组件是可以单独打包测试的,那就说明每一个模块都可以单独运行,该怎么做呢?

首先,在项目目录下的gradle.properties文件中(gradle的配置项,所有的gradle都可以访问到),定义一个变量:

Android模块化和组件 android组件化方案_ARouter_07

isRelease, 如果是true,代表是集成环境,模块不可单独运行;如果改成了false,代表是开发环境,所有moduel都可以单独运行。

那么重点来了,看看app作为可运行module, 和其他module的区别:

Android模块化和组件 android组件化方案_Android模块化和组件_08

Android模块化和组件 android组件化方案_组件化_09

我们发现app模块引入了application插件,业务组件引入了library插件,这就说明如果我们要让业务组件可运行,也要修改成application才可以 ,如果要让gradle自动配置,上面的isRelease就派上了用场:

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

每个业务组件都进行这样的配置,以后切换环境只需要修改isRelease即可。当然,这还不够,我们知道项目都是有包名的,组件怎么配置包名呢?很简单,只需要在每个组件的defaultConfig中这样配置即可

defaultConfig {
    if (!isRelease.toBoolean()) {
        applicationId 'com.kangf.art.goods'
    }
    minSdkVersion rootProject.ext.android.minSdkVersion
    targetSdkVersion rootProject.ext.android.targetSdkVersion
    versionCode rootProject.ext.android.versionCode
    versionName rootProject.ext.android.versionName

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    consumerProguardFiles "consumer-rules.pro"
}

注意每个模块的applicationId尽量不要一样哦。最后还有最重要的一点,manifest的配置,开发和集成环境肯定是不能使用同一个manifest了,当组件需要单独打包时,需要配置theme,项目主入口等,当组件作为library时,就不需要了,那我们先把app工程的资源全部移动到base中,以便所有module都能使用,然后在main目录下建立一个debug目录,用于存放开发环境的manifest文件:

Android模块化和组件 android组件化方案_组件化_10

然后手动去指定资源文件,同时我们希望集成环境不要打包开发环境的manifest:

sourceSets {
        main {
            if (!isRelease.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    //release 时 debug 目录下文件不需要合并到主工程
                    exclude '**/debug/**'
                }
            }
        }
    }

这样配置就完成了,下面把isRelease改为false,来编译一下试试:

Android模块化和组件 android组件化方案_android_11

可以看到,这里有三个可运行的项目了,怎么检测debug有没有被打包进去呢?先在 debug目录创建一个DebugActivity, 作为启动activity

Android模块化和组件 android组件化方案_类加载_12

运行一下order模块看看apk里面:

Android模块化和组件 android组件化方案_类加载_13

记住order里面是有这个activity的,没有问题,现在改为集成环境,运行一下app,看看DebugActivity有没有被打包进去呢?

Android模块化和组件 android组件化方案_ARouter_14

可以看到,没有debug包了,还是不信?看一下manifest:

Android模块化和组件 android组件化方案_类加载_15

我们的DebugActivity不翼而飞,这样开发环境和集成环境的自动部署就算完成了。

二、组件之间的通信

组件之间的通信必不可少,完成消息传递,页面跳转,参数携带等都属于组件通信,目前有以下几种通信方案:

  • 使用 EventBus的方式,缺点是:EventBean维护成本太高,不好去管理:
  • 使用广播的方式,缺点是:不好管理,都统一发出去了
  • 使用隐士意图方式,缺点是:在AndroidManifest.xml里面配置xml写的太多了
  • 使用类加载方式,缺点就是,容易写错包名,类名,缺点较少
  • 使用全局Map的方式,缺点是,要注册很多的对象

今天我们主要从ARouter路由的角度出发,由浅入深,了解ARouter的通信机制

1. 类加载

首先使用类加载的方式,从商品模块跳转订单:

class GoodsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_goods)

        find<TextView>(R.id.tvGoods).setOnClickListener {
            val clazz = Class.forName("com.kangf.art.order.OrderActivity")
            startActivity(Intent(this, clazz))
        }
    }
}

类加载很简单,通过反射获取到class,直接跳转即可:


Android模块化和组件 android组件化方案_类加载_16

2. 全局Map的方式

使用类加载的方式缺点也很明显,通过包名 + 类名直接反射到类,一不小心就会写错,开发起来也是一件很痛苦的事情,所以就有了这种方式的演进,通过一个全局的Map,提前将所有的类都保存起来,用到的时候再取。

Android模块化和组件 android组件化方案_android_17

先说一下这种方案的思路,每个模块都相当于一个组(group),每个组里面由于有多个Activity, 所以每个Activity又维护了一个路径(path),当我们要跳转的时候,通过group找到对应的模块,再通过path找到具体的class。

我们先把class做一层包装:

data class RouteBean(
    // order/order_list
    var path: String? = null,

    // OrderActivity.class
    var clazz: Class<*>? = null
)

下面就开始全局map的定义了:

class RecordPathManager {

    companion object {

        /**
         * 组名:order, order=[{order_path : OrderDetailActivity.class}, {order_list : OrderListActivity.class}]
         */
        private val mMap = mutableMapOf<String, MutableList<RouteBean>>()

        /**
         * 添加一个activity
         */
        fun addRoutePath(group: String, path: String, clazz: Class<*>) {
        	// 先通过group找到对应的list 
            var list = mMap[group]
            
            // 如果list为空,那么就创建一个,把它放到map里面
            if (list == null) {
                list = mutableListOf()
                mMap[group] = list
            }
            
            // 往list中添加数据
            list.add(RouteBean(path, clazz))
        }


        /**
         * 跳转activity
         */
        fun startActivity(group: String, path: String): Class<*>? {
            val list = mMap[group]
            if (list.isNullOrEmpty()) {
                return null
            }

			// 遍历查找list里面的对应的path, 返回其class
            for (routeBean in list) {
                if (routeBean.path == path) {
                    return routeBean.clazz
                }
            }

            return null
        }
    }
}

上面的代码就很简单了,这里就不多说了,然后application里面注册一下:

class App : Application() {

    override fun onCreate() {
        super.onCreate()

        RecordPathManager.addRoutePath("order", "order/list", OrderActivity::class.java)
        RecordPathManager.addRoutePath("goods", "goods/list", GoodsActivity::class.java)
    }
}

最后修改一下跳转方式:

find<TextView>(R.id.tvGoods).setOnClickListener {

            val clazz = RecordPathManager.startActivity("order", "order/list")
            startActivity(Intent(this, clazz))


            // 类加载
//            val clazz = Class.forName("com.kangf.art.order.OrderActivity")
//            startActivity(Intent(this, clazz))
}


Android模块化和组件 android组件化方案_类加载_18

可以看到,正常跳转了!

ARouter

这其实就是ARouter路由框架最基本的原理,我们这样写肯定不是最好的,每个Activity都需要再application中注册,我们是否可以将这些重复的工作交给编译器来解决呢?答案是肯定的,下一篇我们重点讲讲ARouter的黑科技,看看它是怎么自动将activity注入到Map集合中的。