Android组件化框架搭建

前言

组件化是什么,是把一个功能完整的 App 或模块拆分成多个子模块, 表现在androidStudio项目工程里就是分多个module。每个子模块可以独立编译和运行, 模块之间可以任意组合成另一个新的 App 或模块, 每个模块不必须相互依赖但可以相互调起和通信。
组件化的意义,对于一个小型项目来说可能觉得多此一举,但是对于一个中型以上的项目,组件化还是非常有意义的。APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,的代码也变的越来越多,代码耦合严重,影响开发效率,增加项目的维护成本,编译代码的时间长,每修改一处代码后都要重新编译整个项目。组件化的出现就是解决以上问题,并且还具备自由组装成新的模块的优点。
我研究组件化主要还是为了技术储备和支持新业务的开发,对于主项目要使用的话,就相当于项目重构,比较费时费力,在各组都有KPI的情况下是不会给我们时间重构的,闲话不多说,下面介绍组件化框架和搭建过程中注意的问题。

组件化架构

直接上图:


21.png

架构介绍:
如图一共分了4层:应用层、业务组件、功能组件、基础组件。

  • 应用层:我们的app壳工程,负责管理各个业务组件,和打包apk,没有具体的业务功能。
  • 业务组件:具体业务而独立形成一个的工程,可以单独运行提供功能。
  • 功能组件:APP的某些基础功能,可单独编译,但不会单独发布提供功能apk。
  • 基础组件:开源的第三方的库。

Eventbus 是用来各层之间的通信,组件路由是用来实现组件之间的通信和调起。
网上有的文章有不同的分层发,有人分三层把业务组件和功能组件统称组件层。有人对业务组件进行细分,如Main组件。整体是一样的,关键大家能理解整个架构。

组件化项目结构介绍

直接上图:

223.png

大家看到有多个module,以app_开头的是可以单独打包发布的module,也就是app壳module,和业务组件module,module开头的是不单独打包发布的组件,就是功能组件。我们一一说明:

  • app:app壳,就是我们主项目。
  • app_radio:电台业务模块,可以单独作为电台app发布,也是app主项目的一个功能模块。
  • module_core:项目的基础核心组件,说是基础,他是对基础组件的封装,包括架构图中的Glide、Retrofit、Rxjava等的封装,提供基础功能。说是核心,他是对项目基础架构的封装,这里使用的是MVP架构。
  • module_commons:封装组件共用的类和资源,各组件模块解耦之后,避免不了一些共用的类和资源,比如实体类、错误页、dialog等。
  • module_router:封装组件路由,实现组件调起和通信。
  • module_share / module_playserservice:我们项目用到的功能组件,不是必要的。

网上有同学有不同的module分发,比如module_commons进一步细分公共的类module和资源module。大家可以根据实际情况细分。到这里大家会发现组件化的一个弊端,就是项目会有很多的module。

组件化搭建注意问题

组建如何单独编译

思路就是在build.gradle的区分是apply plugin: 'com.android.application'还是apply plugin: 'com.android.library'。具体实现:
在gradle.properties文件添加一个判断属性

isBuildAsModule=false

在build.gradle里根据属性加载不同插件:

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

在两种情况下AndroidManifest.xml文件是有差别的。作为独立运行的app,有自己的Application,要加Launcher的入口intent,而作为library不需要。所以需要写两个不同的AndroidManifest.xml即可,通过isBuildAsModule加以区分。

   if (isBuildAsModule.toBoolean()) {
          manifest.srcFile 'src/main/module/AndroidManifest.xml'
   } else {
         manifest.srcFile 'src/main/library/AndroidManifest.xml'
   }

这种方式同时避免了AndroidManifest.xml清单文件冲突问题。
其他需要区分两种编译模式,原理相同。

基础库和SDK版本号统一

这是多module开发需要解决的问题,不同module的依赖sdk版本不一致或者引入的库版本不一致会导致编译问题和兼容性问题。解决办法是在主项目最外层用一个config.xml文件来统一管理基础库和sdk的版本。config.xml部分代码如下:

def retrofit_version = '2.3.0'
def okhttp_version = '3.9.0'
def dagger_version = '2.11'
def autodispose_version = "0.5.1"
def support_version = '27.1.0'
def espresso_version = '2.2.2'
def glide_version = '4.6.1'
def butterknife_version = '8.8.1'
def routerVersion = "1.2.4"
def routerCompilerVersion = "1.1.4"

project.ext {
    android = [
            compileSdkVersion: 27,
            buildToolsVersion: "27.0.3",
            applicationId    : "com.taihe.music.mvpsample",
            minSdkVersion    : 16,
            targetSdkVersion : 27,
            versionCode      : 1,
            versionName      : "1.0"
    ]

    dependencies = [
            //android-support
            "support-v4"                 : "com.android.support:support-v4:${support_version}",
            "appcompat-v7"               : "com.android.support:appcompat-v7:${support_version}",
         ..........

使用config.xml,时在最外层build.gradle配置config文件:

apply from: "config.gradle"

具体引入基础库的地方在各module的build.gradle文件里:

        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]
.....
   //glide
    api rootProject.ext.dependencies["glide"]
    annotationProcessor rootProject.ext.dependencies["glide-compiler"]
组件之间资源名冲突

组件之间如果资源命名相同,就会产生冲突,解决这个问题最简单的办法就是在项目中制定资源文件命名规范,比如app_radio组件所有资源以radio_开头,所有开发人员必须遵守规范。
当然,gladle也给我提供了解决方案,就是在build.gradle中添加如下的代码:

 resourcePrefix "radio_"

设置了这个属性后,所有的资源名必须以指定的字符串做前缀,否则会报错。而且这种形式只能限定xml里面的资源,并不能限定图片资源,我们仍然需要手动去修改资源名;所以不推荐使用这种方法来解决资源名冲突。

基础组件依赖问题

所有的基础组件我们封装在module_core中,这样就存在一个问题,我们项目依赖了module_core就默认依赖了所有的基础组件,但实际情况中我们是不需要依赖所有的,或者module_core中的基础组件和其他模块有冲突的情况。所以我们需要排除掉用不到和重复的库,Gradle支持两种排除方式,根据组件名排除或者根据包名排除,代码如下:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])    compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
        exclude module: 'support-v4'//根据组件名排除
        exclude group: 'android.support.v4'//根据包名排除
    }
}
组件之间跳转和通信

组件之间的页面调起,虽然我们可以使用intentFilter来实现Activity的隐式启动,但是逻辑上存在耦合,并且不支持Fragment,所以这里我们使用的是阿里的组件路由Arouter。它可以实现Activity和Fragment的调起,支持依赖注入和数据通信。这里我们只介绍Activty调起,详细使用可参考https://blog.csdn.net/zhaoyanjun6/article/details/76165252
我们以调起app_radio组件中的HomeActivity为例,HomeActivity类配置如下:

@Route(path = RouterConstants.RADIO_HOME_ACTIVITY)
public class HomeActivity extends BaseActivity<HomePresenter> implements HomeContract.View {

    @BindView(R2.id.btnUserInfo)
    Button button;
    @BindView(R2.id.ivTest)
    ImageView imageView;
    ........

调起HomeActivity的代码:

ARouter.getInstance()
       .build(RouterConstants.RADIO_HOME_ACTIVITY)
       .withObject("user", userModel)
       .navigation();

可以看出组件路由需要定义常量RouterConstants.RADIO_HOME_ACTIVITY来关联调起页面,withObject("user", userModel)是页面间传递的数据。

手动单独编译

组件化的一个优点就是可以单独编译,但是在开发过程发现有的时候修改了模块module里一些配置文体之后,编译主app,模块module不会重新编译,导致修改无效。所以在修改了配置文件之后最好手动编译module确保修改有效。单独编译的入口在下图:


1538204810(1).png
动态变化网络baseUrl

module_core中我使用Retrofit+OKhttp封装的网络层,并对外提供配置接口,包括配置baseUrl,主module中Applicatuion中会完成这些配置。问题来了,如果其他模块使用baseUrl不同怎么处理。思路就是,对OkHttp进行修改,无非就是使用拦截器,幸好好心人提供了开源库RetrofitUrlManager可以解决该问题。详细参考,https://github.com/JessYanCoding/RetrofitUrlManager
动态修改baseurl的代码:
将OkHttpClient.Builder传给RetrofitUrlManager。

 RetrofitUrlManager.getInstance().with(builder);

以app_radio配置baseUrl为例:

  RetrofitUrlManager.getInstance().putDomain(RADIO_DOMAIN_NAME, RADIO_BASE_API);

RADIO_BASE_API为具体的baseUrl,RADIO_DOMAIN_NAME为对应的key。
具体使用Retrofit来定义请求接口时:

  @Headers({DOMAIN_NAME_HEADER + RADIO_DOMAIN_NAME})
    @GET("users/{user}")
    Maybe<UserInfo> getUserInfo(@Path("user") String user);

这样就可以实现baseUrl的动态变化。同理如果其他的网络配置不同也是使用拦截器是形式来修改,大家可以参考RetrofitUrlManager的实现。
本文从整体上介绍了组件化的搭建问题,没有具体到基础库和框架的封装,也就是module_core的搭建和使用。想要熟悉整体框架或者使用,首先要熟悉第三方库的使用和原理,封装的过程就是改造的过程所以要了解基本原理;其次再熟悉module_core的封装代和使用,最后才是本文提到的组件化过程中的问题解决。
跟本文一样,大部分开源组件化项目,基础库用到了Dagger、Rxjava。这两个开源项目需要一定的学习成本,建议大家择情而定是否使用。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,472评论 2 59
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 第十八回 洞天福地 王洛桢和林阿娇所骑之马,见到狼群后吓得惊慌失措,待二人反应过来时,早已跑得没影了。二人相视苦笑...
    欧陆作文章阅读 504评论 0 0
  • 时光匆匆,年轮写在脸上,却没有写在心里.淡然与挫败充斥着内心. 明天的明天,太阳依旧会在老地方升起,可是今天却与昨...
    首启文阅读 511评论 0 0