什么是组件化
组件化的工作方式信奉独立、完整、自由组合。目标就是尽可能把设计与开发中的元素独立化,使它具备完整的局部功能,通过自由组合来构成整个产品。将每个业务模块分成单独的组件,可单独打包、测试,这种方式能够让我们的项目具有更高的可维护性和可读性。
为什么需要组件化
我们在一些中大型的项目中可以看到,他们少则几个,多则几十个,甚至上百个组件,为什么这样做呢?在早起的项目中,都是单一的模块,进行业务分包的模式开发的,这样随着项目增大,项目失去层次感,维护起来越来越棘手。再一个就是耦合度太高,稍不注意就有不同业务模块的相互调用。组件化的出现,正好可以解决这些问题。
组件化项目结构
这种架构下,主工程就是个空壳子,所有业务模块之间平起平坐,不再相互依赖,如果将来要砍掉某一个模块,可以直接去掉此模块的依赖,省去了大量的无用工作。
组件化项目的实现
组件化项目具体怎么实现呢,这里我们一步一步来操作,假设我们项目中需要一个商品模块和一个订单模块:
- 这两个模块都依赖BaseLibrary
- app作为一个壳工程,依赖 goods和order两个业务模块
一、 gradle优化
我曾经的项目中gradle 都是一团糟,甚至每个组件的sdk版本号都不一样:
现在我们要做的,就是把每个模块的版本号进行统一管理,当升级版本时,改一处即可。
首先需要搞清楚gradle的执行流程
拿这个项目举例:在主工程下面有一个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中应用
进行以上操作之后,我们就可以统一管理每个模块的版本号了:
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都可以访问到),定义一个变量:
isRelease, 如果是true,代表是集成环境,模块不可单独运行;如果改成了false,代表是开发环境,所有moduel都可以单独运行。
那么重点来了,看看app作为可运行module, 和其他module的区别:
[图片上传失败...(image-77a677-1607319390602)][图片上传失败...(image-9efbae-1607319390602)]
我们发现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文件:
然后手动去指定资源文件,同时我们希望集成环境不要打包开发环境的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,来编译一下试试:
[图片上传失败...(image-c2d575-1607319390602)]
可以看到,这里有三个可运行的项目了,怎么检测debug有没有被打包进去呢?先在 debug目录创建一个DebugActivity, 作为启动activity
运行一下order模块看看apk里面:
记住order里面是有这个activity的,没有问题,现在改为集成环境,运行一下app,看看DebugActivity有没有被打包进去呢?
可以看到,没有debug包了,还是不信?看一下manifest:
我们的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,直接跳转即可:
<img src="https://img-blog.csdnimg.cn/20201127140114648.gif#pic_center" alt="在这里插入图片描述" style="zoom: 33%;" />
2. 全局Map的方式
使用类加载的方式缺点也很明显,通过包名 + 类名直接反射到类,一不小心就会写错,开发起来也是一件很痛苦的事情,所以就有了这种方式的演进,通过一个全局的Map,提前将所有的类都保存起来,用到的时候再取。
先说一下这种方案的思路,每个模块都相当于一个组(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))
}
可以看到,正常跳转了!
ARouter
这其实就是ARouter路由框架最基本的原理,我们这样写肯定不是最好的,每个Activity都需要再application中注册,我们是否可以将这些重复的工作交给编译器来解决呢?答案是肯定的,下一篇我们重点讲讲ARouter的黑科技,看看它是怎么自动将activity注入到Map集合中的。