X

Android组件化开发实践和案例分享 | 融合数10个项目模块

作者:杨充

链接:

https://juejin.im/post/5c46e6fb6fb9a049a5713bcc

本文由作者授权发布。

这是我见过写得相当细致的一篇文章了,而且作者的项目非常用心,文章非常长,建议点完收藏继续看。

组件化综合案例,包含微信新闻,头条视频,美女图片,百度音乐,干活集中营,玩Android,豆瓣读书电影,知乎日报等等模块。架构模式:组件化+MVP+Rx+Retrofit+Desgin+Dagger2+阿里VLayout+腾讯X5+腾讯bugly。安装阿里编码规约插件,不断修正不合理代码和最大程度去除黄色警告!

目录介绍

  • 1.为什么要组件化

    • 1.1 为什么要组件化

    • 1.2 现阶段遇到的问题

  • 2.组件化的概念

    • 2.1 什么是组件化

    • 2.2 区分模块化与组件化

    • 2.3 组件化优势好处

    • 2.4 区分组件化和插件化

    • 2.5 application和library

  • 3.创建组件化框架

    • 3.1 传统APP架构图

    • 3.2 组件化需要考虑问题

    • 3.3 架构设计图

    • 3.4 组件通信是通过路由转发

    • 3.5 解决考虑问题

    • 3.6 业务组件的生命周期

    • 3.7 Fragment通信难点

  • 4.实际开发案例

    • 4.1 组件化实践的开源项目

    • 4.1 如何创建模块

    • 4.2 如何建立依赖

    • 4.3 如何统一配置文件

    • 4.4 组件化的基础库

    • 4.5 组件模式和集成模式如何切换

    • 4.6 组件化解决重复依赖

    • 4.7 组件化注意要点

    • 4.8 组件化时资源名冲突

    • 4.9 组件化开发遇到问题

  • 5.组件间通信

    • 5.1 选择那个开源路由库

    • 5.2 阿里Arouter基础原理

    • 5.3 使用Arouter注意事项

  • 6.关于其他

    • 6.1 参考博客链接

    • 6.2 我的博客介绍

    • 6.3 开源项目地址


1为什么要组件化

1.1 为什么要组件化

APP迭代维护成本增高

投资界,新芽,项目工厂等APP自身在飞速发展,版本不断迭代,新功能不断增加,业务模块数量不断增加,业务上的处理逻辑越变越复杂,同时每个模块代码也变得越来越多,这就引发一个问题,所维护的代码成本越来越高,稍微一改动可能就牵一发而动全身,改个小的功能点就需要回归整个APP测试,这就对开发和维护带来很大的挑战。

多人组合需要组件化

APP 架构方式是单一工程模式,业务规模扩大,随之带来的是团队规模扩大,那就涉及到多人协作问题,每个移动端软件开发人员势必要熟悉如此之多代码,如果不按照一定的模块组件机制去划分,将很难进行多人协作开发,随着单一项目变大,而且Andorid项目在编译代码方面就会变得非常卡顿,在单一工程代码耦合严重,每修改一处代码后都需要重新编译打包测试,导致非常耗时。


1.2 现阶段遇到的问题


结合投资界,新芽客户端分析

1. 代码量膨胀,不利于维护,不利于新功能的开发。项目工程构建速度慢,在一些电脑上写两句代码,重新编译整个项目,测试的话编译速度起码 10-20 分钟,有的甚至更长。

2. 不同模块之间代码耦合严重,有时候修改一处代码而牵动许多模块。每个模块之间都有引用第三方库,但有些第三方库版本不一致,导致打包APP时候代码冗余,容易引起版本冲突。

3. 现有项目基于以前其他人项目基础上开发,经手的人次过多,存在着不同的代码风格,项目中代码规范乱,类似的功能写法却不一样,导致不统一。

2组件化的概念


2.1 什么是组件化

什么是组件化呢?

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

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

2.2 区分模块化与组件化

模块化

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

组件化

组件化就是更关注可复用性,更注重关注点分离,如果从集合角度来看的话,可以说往往一个模块包含了一个或多个组件,或者说模块是一个容器,由组件组装而成。

简单来说,组件化相比模块化粒度更小,两者的本质思想都是一致的,都是把大往小的方向拆分,都是为了复用和解耦,只不过模块化更加侧重于业务功能的划分,偏向于复用,组件化更加侧重于单一功能的内聚,偏向于解耦。


2.3 组件化优势好处



简单来说就是提高工作效率,解放生产力,好处如下:

1.提高编译速度,从而提高并行开发效率。

问题:那么如何提高编译速度的呢?组件化框架可以使模块单独编译调试,可以有效地减少编译的时间。

2.稳定的公共模块采用依赖库方式

提供给各个业务线使用,减少重复开发和维护工作量。代码简洁,冗余量少,维护方便,易扩展新功能。

3.每个组件有自己独立的版本,可以独立编译、测试、打包和部署。

  • 针对开发程序员多的公司,组件化很有必要,每个人负责自己的模块,可以较少提交代码冲突。

  • 为新业务随时集成提供了基础,所有业务可上可下,灵活多变。

  • 各业务线研发可以互不干扰、提升协作效率,并控制产品质量。

4.避免模块之间的交叉依赖,做到低耦合、高内聚。

5.引用的第三方库代码统一管理,避免版本统一,减少引入冗余库。

这个可以创建一个公共的gradle管理的文件,比如一个项目有十几个组件,想要改下某个库或者版本号,总不至于一个个修改吧。这个时候提取公共十分有必要

6.定制项目可按需加载,组件之间可以灵活组建,快速生成不同类型的定制产品。


2.4 区分组件化和插件化

组件化和插件化的区别

  1. 组件化不是插件化,插件化是在【运行时】,而组件化是在【编译时】。换句话说,插件化是基于多APK的,而组件化本质上还是只有一个 APK。

  2. 组件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的。

组件化的目标

组件化的目标之一就是降低整体工程(app)与组件的依赖关系,缺少任何一个组件都是可以存在并正常运行的。app主工程具有和组件进行绑定和解绑的功能。

2.5 application和library

在studio中,对两种module进行区分,如下所示

  • 一种是基础库library,比如常见第三方库都是lib,这些代码被其他组件直接引用。

  • 另一种是application,也称之为Component,这种module是一个完整的功能模块。比如分享module就是一个Component。

  • 为了方便,统一把library称之为依赖库,而把Component称之为组件,下面所讲的组件化也主要是针对Component这种类型。

在项目的build.gradle文件中

//控制组件模式和集成模式if (rootProject.ext.isDouBanApplication) {    //是Component,可以独立运行    apply plugin: 'com.android.application'} else {    //是lib,被依赖    apply plugin: 'com.android.library'}
if (rootProject.ext.isDouBanApplication) {
    //是Component,可以独立运行
    apply plugin: 'com.android.application'
else {
    //是lib,被依赖
    apply plugin: 'com.android.library'
}


2.6 注意第三方sdk拆分问题

看了很多博客,几乎没有博客说出在拆分业务组件时,遇到第三方sdk集成的问题。比如:当你的app可以使用微信登陆,在app主工程时,登陆是正常的,这个时候你是通过主工程app的包名去微信开放平台申请id和key值。

但是当你将登陆注册拆分出独立的业务组件时,则该组件的包名是跟app主工程包名不一样的,那么这个时候,如果切换成组件模式则第三方登陆就有可能出现问题。

也就是说,你使用某些第三方sdk时,当初用app的包名去申请得到key值[这个值是根据包名生成的],然后当你拆分业务组件时,自然组件包名和app包名不一样,那么当切换成组件application可以独立运行时,则可能会出现bug,由包名导致的问题。

个人建议,涉及到第三方sdk拆分,可以封装成lib被依赖即可,或者你刻意把包名弄成一样的。


3组件化的概念

3.1 传统APP架构图

传统APP架构图,如图所示,从网上摘来的……

存在的问题

普遍使用的 Android APP 技术架构,往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。单一工程模型下的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分离。如下图:



3.2 组件化需要考虑问题


分而治之,并行开发,一切皆组件。要实现组件化,无论采用什么样的技术方式,需要考虑以下七个方面问题:


代码解耦。


如何将一个庞大的工程分成有机的整体?这个需要一步步来了!

对已存在的项目进行模块拆分,模块分为两种类型,一种是功能组件模块,封装一些公共的方法服务等,作为依赖库对外提供;另一种是业务组件模块,专门处理业务逻辑等功能,这些业务组件模块最终负责组装APP。


组件单独运行。


因为每个组件都是高度内聚的,是一个完整的整体,如何让其单独运行和调试

通过 Gradle脚本配置方式,进行不同环境切换,我自己操作是添加一个boolean值的开关。比如只需要把 Apply plugin: ‘com.android.library’ 切换成Apply plugin: ‘com.android.application’ 就可以独立运行呢!

需要注意:当切换到application独立运行时,需要在AndroidManifest清单文件上进行设置,因为一个单独调试需要有一个入口的Activity。


组件间通信。


由于每个组件具体实现细节都互相不了解,但每个组件都需要给其他调用方提供服务,那么主项目与组件、组件与组件之间如何通信就变成关键?

这个我是直接用阿里开源的路由框架,当然你可以根据需要选择其他大厂的开源路由库。引用阿里的ARouter框架,通过注解方式进行页面跳转。


组件生命周期。


这里的生命周期指的是组件在应用中存在的时间,组件是否可以做到按需、动态使用、因此就会涉及到组件加载、卸载等管理问题。


集成调试。


在开发阶段如何做到按需编译组件?一次调试中可能有一两个组件参与集成,这样编译时间就会大大降低,提高开发效率。


代码隔离。


组件之间的交互如果还是直接引用的话,那么组件之间根本没有做到解耦,如何从根本上避免组件之间的直接引用?目前做法是主项目和业务组件都会依赖公共基础组件库,业务组件通过路由服务依赖库按需进行查找,用于不同组件之间的通信。


告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为AAR包集成到“APP壳工程”中,组成一个完整功能的 APP。

3.3 架构设计图

组件化架构图

业务组件之间是独立的,互相没有关联,这些业务组件在集成模式下是一个个 Library,被 APP 壳工程所依赖,组成一个具有完整业务功能的 APP 应用,但是在组件开发模式下,业务组件又变成了一个个Application,它们可以独立开发和调试,由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可以显著减少编译时间。



3.4 组件通信是通过路由转发

传统以前工程下模块

记得刚开始进入Android开发工作时,只有一个app主工程,后期几乎所有的需求都写在这个app主工程里面。只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。

导致后期改项目为组件化的时候十分痛苦,不同模块之间的业务逻辑实在关联太多,但还是没办法,于是目录4步骤一步步实践。终极目标是,告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发。

组件化模式下如何通信

这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系。在这个开源项目中,我使用的阿里开源的路由框架。关于Arouter基础使用和代码分析,可以看我这篇博客:

Arouter使用与代码解析

https://github.com/yangchong211/YCBlogs



3.6 业务组件的生命周期

按照理想状态的来看待的话

各个业务组件之间没有任何依赖关系,这时我们可以把每个独立的业务组件看成一个可运行的app,所以业务组件的生命周期和应与独立的app保持一致。


3.7 Fragment通信难点

在网上看到很多博客说,如何拆分组件,按模块拆分,或者按照功能拆分。但很少有提到fragment在拆分组件时的疑问,这个让我很奇怪。

先来说一个业务需求,比如一个购物商城app,有4个模块,做法一般是一个activity+4个fragment,这个大家都很熟悉,这四个模块分别是:首页,发现,购物车,我的。然后这几个页面是用fragment写的,共用一个宿主activity,那么在做组件化的时候,我想把它按照业务拆分成首页,发现,购物车和我的四个独立的业务模块。

遇到疑问:

  1. 如果是拆分成四个独立的业务模块,那么对应的fragment肯定要放到对应的组件中,那么这样操作,当主工程与该业务组件解绑的情况下,如何拿到fragment和传递参数进行通信。

  2. Fragment 中 开启Activity带requestCode,开启的Activity关闭后,不会回调Fragment中的onActivityResult。只会调用Fragment 所在Activity的onActivityResult。

  3. 多fragment单activity拦截器不管用,难道只能用于拦截activity的跳转?那如果是要实现登录拦截的话,那不是只能在PathReplaceService中进行了?

网络解决办法

  1. 第一个疑问:由于我使用阿里路由,所以我看到zhi1ong大佬说:用Router跳转到这个Activity,然后带一个参数进去,比方说tab=2,然后自己在onCreate里面自行切换。但后来尝试,还是想问问广大程序员有没有更好的办法。

  2. 第二个疑问:还是zhi1ong大佬说,通过广播,或者在Activity中转发这个事件,比方说让Fragment统一依赖一个接口,然后在Activity中转发。


4实际开发案例

4.1 组件化实践的开源项目

关于组件化开发一点感想

关于网上有许多关于组件化的博客,讲解了什么是组件化,为何要组件化,以及组件化的好处。大多数文章提供了组件化的思路,给我着手组件化开发提供了大量的便利。感谢前辈大神的分享!虽然有一些收获,但是很少有文章能够给出一个整体且有效的方案,或者一个具体的Demo。

但是毕竟看博客也是为了实践做准备,当着手将之前的开源案例改版成组件化案例时,出现了大量的问题,也解决了一些问题。主要是学些了组件化开发流程。

大多数公司慢慢着手组件化开发,在小公司,有的人由于之前没有做过组件化开发,尝试组件化也是挺好的;在大公司,有的人一去只是负责某个模块,可能刚开始组件化已经有人弄好了,那学习实践组件化那更快一些。业余实践,改版之前开源项目,写了这篇博客,耗费我不少时间,要是对你有些帮助,那我就很开心呢。由于我也是个小人物,所以写的不好,勿喷,欢迎提出建议!

关于组件化开源项目

项目整体架构模式采用:组件化+MVP+Rx+Retrofit+design+Dagger2+VLayout+X5

包含的模块:wanAndroid【kotlin】+干货集中营+知乎日报+番茄Todo+精选新闻+豆瓣音乐电影小说+小说读书+简易记事本+搞笑视频+经典游戏+其他更多等等

此项目属于业余时间练手的项目,接口数据来源均来自网络,如果存在侵权情况,请第一时间告知。本项目仅做学习交流使用,API数据内容所有权归原作公司所有,请勿用于其他用途。

关于开源组件化的项目地址:

https://github.com/yangchong211/LifeHelper


4.1 如何创建模块


根据3.3 架构设计图可以知道

主工程:

除了一些全局配置和主 Activity 之外,不包含任何业务代码。有的也叫做空壳app,主要是依赖业务组件进行运行。

业务组件:

最上层的业务,每个组件表示一条完整的业务线,彼此之间互相独立。原则上来说:各个业务组件之间不能有直接依赖!所有的业务组件均需要可以做到独立运行的效果。对于测试的时候,需要依赖多个业务组件的功能进行集成测试的时候。可以使用app壳进行多组件依赖管理运行。

该案例中分为:干活集中营,玩Android,知乎日报,微信新闻,头条新闻,搞笑视频,百度音乐,我的记事本,豆瓣音乐读书电影,游戏组件等等。

功能组件:

该案例中分为,分享组件,评论反馈组件,支付组件,画廊组件等等。同时注意,可能会涉及多个业务组件对某个功能组件进行依赖!

基础组件:

支撑上层业务组件运行的基础业务服务。此部分组件为上层业务组件提供基本的功能支持。

该案例中:在基础组件库中主要有,网络请求,图片加载,通信机制,工具类,分享功能,支付功能等等。当然,我把一些公共第三方库放到了这个基础组件中!


4.2 如何建立依赖

关于工程中组件依赖结构图如下所示


业务模块下完整配置代码

//控制组件模式和集成模式if (rootProject.ext.isGankApplication) {    apply plugin: 'com.android.application'} else {    apply plugin: 'com.android.library'}android {    compileSdkVersion rootProject.ext.android["compileSdkVersion"]    buildToolsVersion rootProject.ext.android["buildToolsVersion"]    defaultConfig {        minSdkVersion rootProject.ext.android["minSdkVersion"]        targetSdkVersion rootProject.ext.android["targetSdkVersion"]        versionCode rootProject.ext.android["versionCode"]        versionName rootProject.ext.android["versionName"]        if (rootProject.ext.isGankApplication){            //组件模式下设置applicationId            applicationId "com.ycbjie.gank"        }        javaCompileOptions {            annotationProcessorOptions {                arguments = [AROUTER_MODULE_NAME: project.getName()]            }        }    }    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }    //jdk1.8    compileOptions {        sourceCompatibility JavaVersion.VERSION_1_8        targetCompatibility JavaVersion.VERSION_1_8    }    sourceSets {        main {            if (rootProject.ext.isGankApplication) {                manifest.srcFile 'src/main/module/AndroidManifest.xml'            } else {                manifest.srcFile 'src/main/AndroidManifest.xml'            }            jniLibs.srcDirs = ['libs']        }    }}dependencies {    implementation fileTree(dir: 'libs', include: ['*.jar'])    implementation project(':library')    annotationProcessor rootProject.ext.dependencies["router-compiler"]}
if (rootProject.ext.isGankApplication) {
    apply plugin: 'com.android.application'
else {
    apply plugin: 'com.android.library'
}

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]


    defaultConfig {
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]

        if (rootProject.ext.isGankApplication){
            //组件模式下设置applicationId
            applicationId "com.ycbjie.gank"
        }
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    //jdk1.8
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    sourceSets {
        main {
            if (rootProject.ext.isGankApplication) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
            jniLibs.srcDirs = ['libs']
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs'include: ['*.jar'])
    implementation project(':library')
    annotationProcessor rootProject.ext.dependencies["router-compiler"]
}

4.3 如何统一配置文件


由于组件化实践中模块比较多,因此配置gradle,添加依赖库时,需要考虑简化工作。那么究竟如何做呢?

第一步,首先在项目根目录下创建一个yc.gradle文件。实际开发中只需要更改该文件中版本信息即可。


我在网上看到的绝大多数案例,都是通过一个开关控件组件模式和集成模式的切换,但是这里我配置了多个组件的开关,分别控制对应的组件切换状态。

ext {    isApplication = false  //false:作为Lib组件存在, true:作为application存在    isAndroidApplication = false  //玩Android模块开关,false:作为Lib组件存在, true:作为application存在    isLoveApplication = false  //爱意表达模块开关,false:作为Lib组件存在, true:作为application存在    isVideoApplication = false  //视频模块开关,false:作为Lib组件存在, true:作为application存在    isNoteApplication = false  //记事本模块开关,false:作为Lib组件存在, true:作为application存在    isBookApplication = false  //book模块开关,false:作为Lib组件存在, true:作为application存在    isDouBanApplication = false  //豆瓣模块开关,false:作为Lib组件存在, true:作为application存在    isGankApplication = false  //干货模块开关,false:作为Lib组件存在, true:作为application存在    isMusicApplication = false  //音乐模块开关,false:作为Lib组件存在, true:作为application存在    isNewsApplication = false  //新闻模块开关,false:作为Lib组件存在, true:作为application存在    isToDoApplication = false  //todo模块开关,false:作为Lib组件存在, true:作为application存在    isZhiHuApplication = false  //知乎模块开关,false:作为Lib组件存在, true:作为application存在    isOtherApplication = false  //其他模块开关,false:作为Lib组件存在, true:作为application存在    android = [               compileSdkVersion       : 28,               buildToolsVersion       : "28.0.3",               minSdkVersion           : 17,               targetSdkVersion        : 28,               versionCode             : 22,               versionName             : "1.8.2"    //必须是int或者float,否则影响线上升级    ]    version = [               androidSupportSdkVersion: "28.0.0",               retrofitSdkVersion      : "2.4.0",               glideSdkVersion         : "4.8.0",               canarySdkVersion        : "1.5.4",               constraintVersion       : "1.0.2"    ]    dependencies = [                //support                "appcompat-v7"             : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",                "multidex"                 : "com.android.support:multidex:1.0.1",                //network                "retrofit"                 : "com.squareup.retrofit2:retrofit:${version["retrofitSdkVersion"]}",                "retrofit-converter-gson"  : "com.squareup.retrofit2:converter-gson:${version["retrofitSdkVersion"]}",                "retrofit-adapter-rxjava"  : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitSdkVersion"]}",                //这里省略一部分代码        ]}
    isApplication = false  //false:作为Lib组件存在, true:作为application存在
    isAndroidApplication = false  //玩Android模块开关,false:作为Lib组件存在, true:作为application存在
    isLoveApplication = false  //爱意表达模块开关,false:作为Lib组件存在, true:作为application存在
    isVideoApplication = false  //视频模块开关,false:作为Lib组件存在, true:作为application存在
    isNoteApplication = false  //记事本模块开关,false:作为Lib组件存在, true:作为application存在
    isBookApplication = false  //book模块开关,false:作为Lib组件存在, true:作为application存在
    isDouBanApplication = false  //豆瓣模块开关,false:作为Lib组件存在, true:作为application存在
    isGankApplication = false  //干货模块开关,false:作为Lib组件存在, true:作为application存在
    isMusicApplication = false  //音乐模块开关,false:作为Lib组件存在, true:作为application存在
    isNewsApplication = false  //新闻模块开关,false:作为Lib组件存在, true:作为application存在
    isToDoApplication = false  //todo模块开关,false:作为Lib组件存在, true:作为application存在
    isZhiHuApplication = false  //知乎模块开关,false:作为Lib组件存在, true:作为application存在
    isOtherApplication = false  //其他模块开关,false:作为Lib组件存在, true:作为application存在

    android = [
               compileSdkVersion       : 28,
               buildToolsVersion       : "28.0.3",
               minSdkVersion           : 17,
               targetSdkVersion        : 28,
               versionCode             : 22,
               versionName             : "1.8.2"    //必须是int或者float,否则影响线上升级
    ]

    version = [
               androidSupportSdkVersion: "28.0.0",
               retrofitSdkVersion      : "2.4.0",
               glideSdkVersion         : "4.8.0",
               canarySdkVersion        : "1.5.4",
               constraintVersion       : "1.0.2"
    ]

    dependencies = [
                //support
                "appcompat-v7"             : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
                "multidex"                 : "com.android.support:multidex:1.0.1",
                //network
                "retrofit"                 : "com.squareup.retrofit2:retrofit:${version["retrofitSdkVersion"]}",
                "retrofit-converter-gson"  : "com.squareup.retrofit2:converter-gson:${version["retrofitSdkVersion"]}",
                "retrofit-adapter-rxjava"  : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitSdkVersion"]}",
                //这里省略一部分代码
        ]
}


第二步,然后在项目中的lib【注意这里是放到基础组件库的build.gradle】中添加代码,如下所示

apply plugin: 'com.android.library'android {    compileSdkVersion rootProject.ext.android["compileSdkVersion"]    buildToolsVersion rootProject.ext.android["buildToolsVersion"]    defaultConfig {        minSdkVersion rootProject.ext.android["minSdkVersion"]        targetSdkVersion rootProject.ext.android["targetSdkVersion"]        versionCode rootProject.ext.android["versionCode"]        versionName rootProject.ext.android["versionName"]    }}dependencies {    implementation fileTree(dir: 'libs', include: ['*.jar'])    api rootProject.ext.dependencies["appcompat-v7"]    api rootProject.ext.dependencies["design"]    api rootProject.ext.dependencies["palette"]    api rootProject.ext.dependencies["glide"]    api (rootProject.ext.dependencies["glide-transformations"]){        exclude module: 'glide'    }    annotationProcessor rootProject.ext.dependencies["glide-compiler"]    api files('libs/tbs_sdk_thirdapp_v3.2.0.jar')    api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"    //省略部分代码}'com.android.library'

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]


    defaultConfig {
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]
    }
}


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api rootProject.ext.dependencies["appcompat-v7"]
    api rootProject.ext.dependencies["design"]
    api rootProject.ext.dependencies["palette"]
    api rootProject.ext.dependencies["glide"]
    api (rootProject.ext.dependencies["glide-transformations"]){
        exclude module: 'glide'
    }
    annotationProcessor rootProject.ext.dependencies["glide-compiler"]
    api files('libs/tbs_sdk_thirdapp_v3.2.0.jar')
    api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    //省略部分代码
}


第三步,在其他model中添加依赖

implementation project(‘:library’)即可。


4.4 组件化的基础库

基础库组件封装

基础库组件封装库中主要包括开发常用的一些框架。可以直接看我的项目更加直观!

1、网络请求(采用Retrofit+RxJava框架),拦截器

2、图片加载(策略模式,Glide与Picasso之间可以切换)

3、通信机制(RxBus),路由ARouter简单封装工具类(不同model之间通信)

4、mvp框架,常用的base类,比如BaseActivity,BaseFragment等等

5、通用的工具类,比如切割圆角,动画工具类等等

6、自定义view(包括对话框,ToolBar布局,圆形图片等view的自定义)

7、共有的shape,drawable,layout,color等资源文件

8、全局初始化异步线程池封装库,各个组件均可以用到

组件初始化

比如,你将该案例中的新闻组件切换成独立运行的app,那么由于新闻跳转详情页需要使用到x5的WebView,因此需要对它进行初始化。最刚开始做法是,为每一个可以切换成app的组件配置一个独立的application,然后初始化一些该组件需要初始化的任务。

但是这么做,有一点不好,不是很方便管理。后来看了知乎组件化实践方案后,该方案提出,开发了一套多线程初始化框架,每个组件只要新建若干个启动 Task 类,并在 Task 中声明依赖关系。但是具体怎么用到代码中后期有待实现!

如何简化不熟悉组件化的人快速适应组件独立运行

设置多个组件开关,需要切换那个组件就改那个。如果设置一个开关,要么把所有组件切成集成模式,要么把所有组件切成组件模式,有点容易出问题。更多可以往下看!

严格限制公共基础组件的增长

随着开发不断进行,要注意不要往基础公共组件加入太多内容。而是应该减小体积!倘若是基础组件过于庞大,那么运行组件也是比较缓慢的!


4.5 组件模式和集成模式如何切换

在玩Android组件下的build.gradle文件,其他组件类似。

通过一个开关来控制这个状态的切换,module如果是一个库,会使用com.android.library插件;如果是一个应用,则使用com.android.application插件


//控制组件模式和集成模式if (rootProject.ext.isAndroidApplication) {    apply plugin: 'com.android.application'} else {    apply plugin: 'com.android.library'}
if (rootProject.ext.isAndroidApplication) {
    apply plugin: 'com.android.application'
else {
    apply plugin: 'com.android.library'
}


集成模式如下所示


首先需要在yc.gradle文件中设置 isApplication=false。Sync下后,发现该组件是library

ext {    isAndroidApplication = false  //false:作为Lib组件存在, true:作为application存在
    isAndroidApplication = false  //false:作为Lib组件存在, true:作为application存在


组件模式如下所示


首先需要在yc.gradle文件中设置 isApplication=true。Sync下后,发现该组件是application,即可针对模块进行运行

ext {    isAndroidApplication = true  //false:作为Lib组件存在, true:作为application存在
    isAndroidApplication = true  //false:作为Lib组件存在, true:作为application存在

需要注意的地方,这个很重要


首先看看网上绝大多数的作法,非常感谢这些大神的无私奉献!但是我觉得多个组件用一个开关控制也可以,但是sourceSets里面切换成组件app时,可以直接不用下面这么麻烦,可以复用java和res文件。


接下来看看我的做法:

下面这个配置十分重要。也就是说当该玩Android组件从library切换到application时,由于可以作为独立app运行,所以序意设置applicationId,并且配置清单文件,如下所示!

在 library 和 application 之间切换,manifest文件也需要提供两套


android {    defaultConfig {        if (rootProject.ext.isAndroidApplication){            //组件模式下设置applicationId            applicationId "com.ycbjie.android"        }    }    sourceSets {        main {            if (rootProject.ext.isAndroidApplication) {                manifest.srcFile 'src/main/module/AndroidManifest.xml'            } else {                manifest.srcFile 'src/main/AndroidManifest.xml'            }            jniLibs.srcDirs = ['libs']        }    }}
        if (rootProject.ext.isAndroidApplication){
            //组件模式下设置applicationId
            applicationId "com.ycbjie.android"
        }
    }
    sourceSets {
        main {
            if (rootProject.ext.isAndroidApplication) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
            jniLibs.srcDirs = ['libs']
        }
    }
}


具体在项目中如下所示


4.6 组件化解决重复依赖

重复依赖问题说明

重复依赖问题其实在开发中经常会遇到,比如项目 implementation 了一个A,然后在这个库里面又 implementation 了一个B,然后你的工程中又 implementation 了一个同样的B,就依赖了两次。

默认情况下,如果是 aar 依赖,gradle 会自动帮我们找出新版本的库而抛弃旧版本的重复依赖。但是如果使用的是project依赖,gradle并不会去去重,最后打包就会出现代码中有重复的类了。

解决办法,举个例子

api(rootProject.ext.dependencies["logger"]) {     exclude module: 'support-v4'//根据组件名排除     exclude group: 'android.support.v4'//根据包名排除 }
    exclude module: 'support-v4'//根据组件名排除 
    exclude group'android.support.v4'//根据包名排除 
}


4.7 组件化注意要点

业务组件之间联动导致耦合严重

比如,实际开发中,购物车和首页商品分别是两个组件。但是遇到产品需求,比如过节做个活动,发个购物券之类的需求,由于购物车和商品详情页都有活动,因此会造成组件经常会发生联动。倘若前期准备不足,随着时间的推移,各个业务线的代码边界会像组件化之前的主工程一样逐渐劣化,耦合会越来越严重。

第一种解决方式:使用 sourceSets 的方式将不同的业务代码放到不同的文件夹,但是 sourceSets 的问题在于,它并不能限制各个 sourceSet 之间互相引用,所以这种方式并不太友好!

第二种解决方式:抽取需求为工具类,通过不同组件传值而达到调用关系,这样只需要改工具类即可改需求。但是这种只是符合需求一样,但是用在不同模块的场景。

组件化开发之数据库分离

比如,我现在开发的视频模块想要给别人用,由于缓存之类需要用到数据库,难道还要把这个lib还得依赖一个体积较大的第三方数据库?但是使用系统原生sql数据库又不太方便,怎么办?暂时我也没找到办法……


4.8 组件化时资源名冲突


资源名冲突有哪些?

比如,color,shape,drawable,图片资源,布局资源,或者anim资源等等,都有可能造成资源名称冲突。这是为何了,有时候大家负责不同的模块,如果不是按照统一规范命名,则会偶发出现该问题。

尤其是如果string, color,dimens这些资源分布在了代码的各个角落,一个个去拆,非常繁琐。其实大可不必这么做。因为android在build时,会进行资源的merge和shrink。res/values下的各个文件(styles.xml需注意)最后都只会把用到的放到intermediate/res/merged/../valus.xml,无用的都会自动删除。并且最后我们可以使用lint来自动删除。所以这个地方不要耗费太多的时间。

解决办法

这个问题也不是新问题了,第三方SDK基本都会遇到,可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名。

个人建议

将color,shape等放到基础库组件中,因为所有的业务组件都会依赖基础组件库。在styles.xml需注意,写属性名字的时候,一定要加上前缀限定词。假如说不加的话,有可能会在打包成aar后给其他模块使用的时候,会出现属性名名字重复的冲突,为什么呢?因为BezelImageView这个名字根本不会出现在intermediate/res/merged/../valus.xml里, 所以不要以为这是属性的限定词!


4.9 组件化开发遇到问题


如何做到各个组件化模块能获取到全局上下文


情景再现


比如,刚开始线上项目是在app主工程里创建的单利,那么在lib中或者后期划分的组件化,是无法拿到主工程的application类中的上下文。这个时候可以

解决办法


很容易,在lib里写一个Utils工具类,然后在主工程application中初始化Utils.init(this),这样就可以在lib和所有业务组件[已经依赖公共基础组件库]中拿到全局上下文呢!


butterKnife使用问题


尽管网上有不少博客说可以解决butterKnife在不同组件之间的引用。但是我在实际开发时,遇到组件模式和集成模式切换状态时,导致出现编译错误问题。要是那位在组件化中解决butterKnife引用问题,可以告诉我,非常感谢!


当组件化是lib时


不能使用switch(R.id.xx),需要使用if..else来代替。


不要乱发bus消息


如果项目中大量的使用eventbus,那么会看到一个类中有大量的onEventMainThread()方法,写起来很爽,阅读起来很痛苦。

虽然说,前期使用EventBus或者RxBus发送消息来实现组件间通信十分方便和简单,但是随着业务增大,和后期不断更新,有的还经过多个程序员前前后后修改,会使代码阅读量降低。项目中发送这个Event的地方非常多,接收这个Event的地方也很多。在后期想要改进为组件化开发,而进行代码拆分时,都不敢轻举妄动,生怕哪些事件没有被接收。


页面跳转存在问题


如果一个页面需要登陆状态才可以查看,那么会写if(isLogin()){//跳转页面}else{//跳转到登录页面},每次操作都要写这些个相同的逻辑。

原生startActivity跳转,无法监听到跳转的状态,比如跳转错误,成功,异常等问题。

有时候,后台会控制从点击按钮【不同场景下】跳转到不同的页面,假如后台配置信息错误,或者少了参数,那么跳转可能不成功或者导致崩溃,这个也没有一个好的处理机制。

阿里推出的开源框架Arouter,便可以解决页面跳转问题,可以添加拦截,或者即使后台配置参数错误,当监听到跳转异常或者跳转错误时的状态,可以直接默认跳转到首页。我在该开源案例就是这么做的!


关于跳转参数问题


先来看一下这种代码写法,这种写法本没有问题,只是在多人开发时,如果别人想要跳转到你开发模块的某个页面,那么就容易传错值。建议将key这个值,写成静态常量,放到一个专门的类中。方便自己,也方便他人。//跳转

//跳转intent.setClass(this,CommentActivity.class);intent.putExtra("id",id);intent.putExtra("allNum",allNum);intent.putExtra("shortNum",shortNum);intent.putExtra("longNum",longNum);startActivity(intent);//接收Intent intent = getIntent();int allNum = intent.getExtras().getInt("allNum");int shortNum = intent.getExtras().getInt("shortNum");int longNum = intent.getExtras().getInt("longNum");int id = intent.getExtras().getInt("id");
intent.setClass(this,CommentActivity.class);
intent.putExtra("id",id);
intent.putExtra("allNum",allNum);
intent.putExtra("shortNum",shortNum);
intent.putExtra("longNum",longNum);
startActivity(intent);


//接收
Intent intent = getIntent();
int allNum = intent.getExtras().getInt("allNum");
int shortNum = intent.getExtras().getInt("shortNum");
int longNum = intent.getExtras().getInt("longNum");
int id = intent.getExtras().getInt("id");


5组件间通信


5.1 选择那个开源路由库

比较有代表性的组件化开源框架有得到得到DDComponentForAndroid、阿里Arouter、聚美Router 等等。

得到DDComponentForAndroid:一套完整有效的android组件化方案,支持组件的组件完全隔离、单独调试、集成调试、组件交互、UI跳转、动态加载卸载等功能。

阿里Arouter:对页面、服务提供路由功能的中间件,简单且够用好用,网上的使用介绍博客也很多,在该组件化案例中,我就是使用这个。

Router:一款单品、组件化、插件化全支持的路由框架

5.2 阿里Arouter基础原理


这里只是说一下基础的思路


在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activityClass映射关系的类文件,然后app进程启动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址。


添加@Route注解然后编译一下,就可以生成这个类,然后看一下这个类。如下所示:


/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */public class ARouter$$Group$$video implements IRouteGroup {  @Override  public void loadInto(Map<String, RouteMeta> atlas) {    atlas.put("/video/VideoActivity", RouteMeta.build(RouteType.ACTIVITY, VideoActivity.class, "/video/videoactivity", "video", null, -1, -2147483648));  }}
public class ARouter$$Group$$video implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/video/VideoActivity", RouteMeta.build(RouteType.ACTIVITY, VideoActivity.class"/video/videoactivity""video"null-1-2147483648));
  }
}


ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),然后new Intent(),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样便可以实现两个相互没有依赖的module顺利的启动对方的Activity了。


看_ARouter类中的 _navigation方法代码,在345行。

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {    final Context currentContext = null == context ? mContext : context;    switch (postcard.getType()) {        case ACTIVITY:            // Build intent            final Intent intent = new Intent(currentContext, postcard.getDestination());            intent.putExtras(postcard.getExtras());            // Set flags.            int flags = postcard.getFlags();            if (-1 != flags) {                intent.setFlags(flags);            } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);            }            // Set Actions            String action = postcard.getAction();            if (!TextUtils.isEmpty(action)) {                intent.setAction(action);            }            // Navigation in main looper.            runInMainThread(new Runnable() {                @Override                public void run() {                    startActivity(requestCode, currentContext, intent, postcard, callback);                }            });            break;        case PROVIDER:            //这里省略代码        case BOARDCAST:        case CONTENT_PROVIDER:        case FRAGMENT:            //这里省略代码        case METHOD:        case SERVICE:        default:            return null;    }    return null;}
    final Context currentContext = null == context ? mContext : context;

    switch (postcard.getType()) {
        case ACTIVITY:
            // Build intent
            final Intent intent = new Intent(currentContext, postcard.getDestination());
            intent.putExtras(postcard.getExtras());

            // Set flags.
            int flags = postcard.getFlags();
            if (-1 != flags) {
                intent.setFlags(flags);
            } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }

            // Set Actions
            String action = postcard.getAction();
            if (!TextUtils.isEmpty(action)) {
                intent.setAction(action);
            }

            // Navigation in main looper.
            runInMainThread(new Runnable() {
                @Override
                public void run() {
                    startActivity(requestCode, currentContext, intent, postcard, callback);
                }
            });

            break;
        case PROVIDER:
            //这里省略代码
        case BOARDCAST:
        case CONTENT_PROVIDER:
        case FRAGMENT:
            //这里省略代码
        case METHOD:
        case SERVICE:
        default:
            return null;
    }
    return null;
}


5.3 使用Arouter注意事项

使用阿里路由抽取工具类,方便后期维护!

下面这种做法,是方便后期维护。

首先看一下网络上有一种写法。

//首先通过注解添加下面代码@Route(path = "/test/TestActivity")public class TestActivity extends BaseActivity {}//跳转ARouter.getInstance().inject("/test/TestActivity");
@Route(path = "/test/TestActivity")
public class TestActivity extends BaseActivity {

}

//跳转
ARouter.getInstance().inject("/test/TestActivity");


优化后的写法


下面这种做法,是方便后期维护。

//存放所有的路由路径常量public class ARouterConstant {    //跳转到视频页面    public static final String ACTIVITY_VIDEO_VIDEO = "/video/VideoActivity";    //省略部分diamagnetic}//存放所有的路由跳转,工具类public class ARouterUtils {    /**     * 简单的跳转页面     * @param string                string目标界面对应的路径     */    public static void navigation(String string){        if (string==null){            return;        }        ARouter.getInstance().build(string).navigation();    }}//调用@Route(path = ARouterConstant.ACTIVITY_VIDEO_VIDEO)public class VideoActivity extends BaseActivity {}ARouterUtils.navigation(ARouterConstant.ACTIVITY_VIDEO_VIDEO);
public class ARouterConstant {
    //跳转到视频页面
    public static final String ACTIVITY_VIDEO_VIDEO = "/video/VideoActivity";
    //省略部分diamagnetic
}

//存放所有的路由跳转,工具类
public class ARouterUtils {
    /**
     * 简单的跳转页面
     * @param string                string目标界面对应的路径
     */

    public static void navigation(String string){
        if (string==null){
            return;
        }
        ARouter.getInstance().build(string).navigation();
    }
}

//调用
@Route(path = ARouterConstant.ACTIVITY_VIDEO_VIDEO)
public class VideoActivity extends BaseActivity {

}
ARouterUtils.navigation(ARouterConstant.ACTIVITY_VIDEO_VIDEO);


组件化实践项目开源地址:

https://github.com/yangchong211/LifeHelper

                      喜欢 就关注吧,欢迎投稿!



本网站文章均为原创内容,并可随意转载,但请标明本文链接
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/android组件化开发实践和案例分享-融合数10个项目模块/
Categories: Android
admin: