Android组件化方案(一)

什么是组件化

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

为什么需要组件化

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

组件化项目结构

在这里插入图片描述

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

组件化项目的实现

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

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

一、 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))
}
image

可以看到,正常跳转了!

ARouter

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

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

推荐阅读更多精彩内容

  • 前情回顾 上一篇文章我们主要讲的gradle的统一管理,组件之间的通信方案,以及如何使用全局map进行页面跳转。每...
    PanGeng阅读 719评论 2 2
  • 在进行app组件化之前我们要明白什么是组件化?为什么要组件化? 什么是组件化?为什么要组件化? 在项目的体系结构,...
    勤能不能补拙阅读 2,451评论 1 9
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,513评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,559评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,723评论 1 1