Android组件化架构

什么是组件化

组件(Component)是对数据和方法的简单封装,功能单一,高内聚,并且是业务能划分的最小粒度。

组件化是基于组件可重用的目的上,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,使得整个软件系统也做到电路板一样,是单个或多个组件元件组装起来,哪个组件坏了,整个系统可继续运行,而不出现崩溃或不正常现象,做到更少的耦合和更高的内聚。

区分模块化与组件化
  • 模块化
    模块化就是将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块我们相对熟悉,比如登录功能可以是一个模块,搜索功能可以是一个模块等等。

  • 组件化
    组件功能单一,高内聚,是业务能划分的最小粒度。组件化就是更关注可复用性,更注重关注点分离,如果从集合角度来看的话,可以说往往一个模块包含了一个或多个组件,或者说模块是一个容器,由组件组装而成。

组件可以单独成独立项目进行编译运行,通常每个组件由单人或者固定人员负责,别人可能不清楚这个组件下的业务及代码,因为这些组件可以做成远程仓库依赖的方式。

组件化优势

1.提高编译速度,从而提高并行开发效率
2.每个组件有自己独立的版本,可以独立编译、测试、打包和部署
3.避免模块之间的交叉依赖,做到低耦合、高内聚
4.组件之间可以灵活组建,快速生成不同类型的定制产品,可拆可装

组件化需要考虑问题

  • 组件分层
    如何将庞大工程分成有机整体。


  • 组件之间跳转

组件之间是无法相互引用的,所以做跳转和通信需要做处理,实现办法就是路由,业务组件之间相互通信,需要访问基础组件中的路由框架,路由来寻址找到目标页面或者目标功能。

什么是路由?
app的一个页面就可以类比于一个个网站里面的页面,浏览器的每个页面由url定义,给不同url传递不同参数,页面的表现形式还稍有不通过,这里的映射关系就是url对应页面,每个app的每个页面也可以类比于网站的页面,那是不是可以采用url的方式来定义每个页面呢?这样是不是也就有了url对应app页面的映射关系,如果有了这样的映射关系,给定一个url,那是不是就可以知道跳转到某一个具体的Activity了?Android路由也是一个映射表,用来映射Uri和对应的页面跳转,这个url就是组件名+页面名来拼接。

我这里做跳转用的是ARouter。这里是我的另一篇ARouter解析

  • 组件化解决重复依赖

组件从开始设计的时候就需要严格分好依赖层级,组件之间不可相互依赖,不可重复依赖,业务组件只可依赖必须的base组件。主app壳组件依赖其他所有组件。

  • 组件单独运行和集成测试

只需要把 Apply plugin: 'com.android.library' 切换成Apply plugin: 'com.android.application' 。我们可以通过 Gradle脚本配置方式,修改properties配置可让某个组件单独运行。比如使用isDebug变量对两种引用插件进行选择。

集成测试需要空manifest,独立运行需要完善manifest文件,需要用gradle配置进行2个manifest文件切换。


然后将公共配置的gradle代码抽出配置文件进行apply使用。

  • 组件化时资源名冲突

color,shape,drawable,图片资源,布局资源,或者anim资源等等,都有可能造成资源名称冲突。大家都在不同组件下,通常不会交流,有可能造成冲突,所以在项目创建初期,需要定义好公共资源,很少修改。在版本不断升级,业务不断复杂,肯定还是避免不了不同的资源文件,所以需要要有按模块区分命名规则。

  • 组件之间相互调用

组件之间相互通信是少不了的,各组件间不能直接调用。组件之间的交互如果还是直接引用的话,那么组件之间根本没有做到解耦。

需要暴露功能给别的组件调用的组件,需要在公共模块base里面去声明接口,继承ARouter库中的Iprovider接口。然后在自己模块中实现该接口的功能。别的模块直接调用该暴露的接口而实现功能。

在公共模块给各个组件定义一个包,里面创建需要提供给外部使用的接口SwitchFarmProvider,需要继承ARouter提供的IProvider接口。

interface SwitchFarmProvider: IProvider {

    fun showSwitchFarmDialog(pos: Int, fragmentManager: FragmentManager)

}

然后在自己组件中去实现该方法的功能,这样就能提供给外部组件调用内部的方法,而不用将该功能将低到公共组件中。

@Route(path = "/xxx/xxx", name = "xxx")
class SwitchFarmImp: SwitchFarmProvider {

    override fun showSwitchFarmDialog(pos: Int, fragmentManager: FragmentManager) {
        SwitchFarmAllDialog().switchTab(pos).show(fragmentManager, "")
    }

    override fun init(context: Context?) {

    }
}

调用方式,通过ARouter提供的ARouter.getInstance().navigation(Class)方法获取该实现类,调用其公共方法。

ARouter.getInstance().navigation(SwitchFarmProvider.class).showSwitchFarmDialog(2, getFragmentManager());
  • 将组件发布到远程仓库

不同部门的开发分工更加明确之后,不属于自己维护的组件范围不能随意地修改。基本上自己负责自己模块下的组件,尽可能少地改动别的组件代码。这一块的配置是全文终点,敲黑板了。

我这里将各个组件发布到阿里云 maven库中,发布方法见我另一篇文章——发布开源库到阿里云 maven仓库。发布之后,可以看到远程仓库里的库。这里需要注意的是,组件不要依赖本地组件,而是从底层开始逐渐依赖,按照依赖顺序上传,否则很可能会依赖错误。

然后各个模块引入库,在app下都引入远程依赖,在settings.gradle中移除各个组件的include。那么项目文件夹就变为了无本地依赖的状态:

此时你的项目仍然是能够运行起来的,不过编译运行的代码就不是你本地的了,而是直接运行各个远程的aar包。

那么平时开发怎么修改我们本地的代码呢?做配置,可分别设置某个组件是依赖本地还是远程,依赖本地的组件可尽心开发修改,发布上传新的版本。
做法是在各组件下新建gradle.properties读取里面的配置,比如设置true表示依赖远程。在settings.gradle中读取该文件的属性,看是否需要依赖本地的组件。在项目的build.gradle中配置,读取该true/false属性,判断是依赖本地库还是远程库。

settings.gradle中配置:

includeCompat ':module-play'
includeCompat ':module-notice'
includeCompat ':module-community'
includeCompat ':module-user'
includeCompat ':module-login'
includeCompat ':module-home'
includeCompat ':module-entrance'
includeCompat ':library-res'
includeCompat ':library-network'
includeCompat ':library-base'
includeCompat ':app'
rootProject.name = "MvvmFrame"

def includeCompat(String name) {
    if (!isMaven(name)) {
        include(name)
    }
}

def isMaven(String name) {
    println("isMaven" + name)
    Properties properties = new Properties()
    def file = new File("${name.replace(":", "")}/maven.properties")
    if (file.exists()) {
        InputStream inputStream = file.newDataInputStream()
        properties.load(inputStream)
        def str = properties.getProperty('MAVEN')
        if (str == null) {
            return false
        } else {
            return Boolean.parseBoolean(str)
        }
    }
    return false
}

项目的build.gradle中配置:

    //根据是否为远程依赖设置依赖远程库还是本地库
    ext.projectCompat = { name ->
        def realName = name.replace(":", "")
        if (isMaven(realName)) {
            return "com.libo:${realName}:${mavenVersion(realName)}"
        } else {
            return project(name)
        }
    }

    //判断当前组件是否为远程依赖
    ext.isMaven = { name ->
        Properties properties = new Properties()
        def file = rootProject.file("${name}/maven.properties")
        if (file.exists()) {
            InputStream inputStream = file.newDataInputStream()
            properties.load(inputStream)
            def str = properties.getProperty('MAVEN')
            if (str == null) {
                return false
            } else {
                return Boolean.parseBoolean(str)
            }
        }
        return false
    }

    //读取maven.property文件中库版本
    ext.mavenVersion = { name ->
        println("mavenVersion::${name}")
        Properties properties = new Properties()
        def file = rootProject.file("${name}/maven.properties")
        if (file.exists()) {
            InputStream inputStream = file.newDataInputStream()
            properties.load(inputStream)
            def str = properties.getProperty('VERSION')
            if (str == null) {
                throw Exception(file.path + "    VERSION == null")
            } else {
                return str
            }
        }
        return ""
    }

app下的build.gradle中这样依赖,判断是依赖远程还是本地。

    implementation projectCompat(":library-res")
    implementation projectCompat(":library-base")
    implementation projectCompat(":library-network")
    implementation projectCompat(":module-entrance")
    implementation projectCompat(":module-home")
    implementation projectCompat(":module-community")
    implementation projectCompat(":module-notice")
    implementation projectCompat(":module-user")
    implementation projectCompat(":module-login")
    implementation projectCompat(":module-play")

该项目配置的github地址,需要细看的戳这里

参考:
https://www.bilibili.com/video/BV1oK4y1R7Hx?p=9&vd_source=40c24e77b23dc2e50de2b7c87c6fed59

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

推荐阅读更多精彩内容