Android组件化框架搭建

本篇文章已授权微信公众号 hongyangAndroid (鸿洋)独家发布

背景

当一个项目经过N手人开发,N个产品经理的蹂躏,N长时间的维护,此时一定存在大量代码冗余、业务耦合、项目臃肿,资源文件大把重复等等,不堪重负。当需要增加新功能或者修改之前某个功能的时候,我相信很多同仁都说只敢增加,不敢随意的去删除、修改原有的代码,因为不知道哪些有用,哪些没有用。不但增加了维护成本,也在无形中增加了APK的体积,浪费了资源。
在此背景下,就衍生除了模块化、组件化的概念。目前也已经有很多优秀的案例,我就踩在巨人的肩膀上搭建了符合组件业务的组件化框架。

先放一个Demo地址,文章末尾也有

效果图.png

一.浅谈模块

其基本理念就是,把常用的功能、控件、基础类、第三方库、权限等公共部分抽离封装,把业务拆分成N个模块进行独立(module)的管理,而所有的业务组件都依赖于封装的基础库,业务组件之间不做依赖,这样的目的是为了让每个业务模块能单独运行。而在APP层对整个项目的模块进行组装,拼凑成一个完整的APP。借助路由(Arouter)来对各个业务组件之间的跳转,通过消息(eventbus)来做各个业务模块之间的通信。
模块化的好处:

  • 1.解耦 只要封装做得好,实际开发中会省去大量的重复代码的coding。
  • 2.结构清晰、层次明显,对后面的维护也是极其容易。
  • 3.每个业务模块可独立运行,单独提测,节省开发时间。

二.基础搭建

先来一张整个项目构思图


项目构思图

根据项目构思图搭建的项目结构图


项目结构图

下面逐一介绍每个模块的功:

  • app模块 app壳没有任何功能主要就是集成每个业务组件,最终打包成一个完整的APK
    app壳的gradle做如下配置,根据配置文件中的isModule字段来依赖不同的业务组件
...
dependencise{
      //公用依赖包
    implementation project(':common_base')
    if (!Boolean.valueOf(rootProject.ext.isModule)) {
        //main模块
        implementation project(':module_main')
        implementation project(':module_market')
        implementation project(':module_wan_android')
    }
}
...
  • common_base模块功能组件主要负责封装公共部分,如第三方库加载、网络请求、数据存储、自定义控件、各种工具类等。
    为了防止重复依赖问题,所有的第三方库都放在该模块加载,业务模块不在做任何的第三方库依赖,只做common_base库的依赖即可。
    common模块无论在什么情况下都是以library的形式存在,所有的业务组件都必须依赖于common
    其结构如下:
    common_base结构图.png

    在commong的gradle中引入项目中使用的所有第三方库,业务组件就不用再去逐一引入
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

...

dependencies {
    // 在项目中的libs中的所有的.jar结尾的文件,都是依赖
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //把implementation 用api代替,它是对外部公开的, 所有其他的module就不需要添加该依赖
    api rootProject.ext.dependencies["appcompat_v7"]
    api rootProject.ext.dependencies["constraint_layout"]
    api rootProject.ext.dependencies["cardview-v7"]
    api rootProject.ext.dependencies["recyclerview-v7"]
    api rootProject.ext.dependencies["support-v4"]
    api rootProject.ext.dependencies["design"]
    api rootProject.ext.dependencies["support_annotations"]

    //MultiDex分包方法
    api rootProject.ext.dependencies["multidex"]

    //黄油刀
    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
    api rootProject.ext.dependencies["butterknife"]

    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
    api rootProject.ext.dependencies["arouter_api"]
    api rootProject.ext.dependencies["arouter_annotation"]

    //eventbus 发布/订阅事件总线
    api rootProject.ext.dependencies["eventbus"]

    //网络
    api rootProject.ext.dependencies["novate"]

    //日志
    api rootProject.ext.dependencies["logger"]

    //fastJson
    api rootProject.ext.dependencies["fastjson"]

    //沉浸栏
    api rootProject.ext.dependencies["barlibrary"]

    //banner
    api rootProject.ext.dependencies["banner"]

    //图片加载
    api rootProject.ext.dependencies["picasso"]

    //lombok
    api rootProject.ext.dependencies["lombok"]
    api rootProject.ext.dependencies["lombokJavax"]

}
  • 业务组件,在集成模式下它以library的形式存在。在组件开发模式下它以application的形式存在,可以单独独立运行。
    业务组件完整的gradle如下:
if (Boolean.valueOf(rootProject.ext.isModule)) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'com.jakewharton.butterknife'

 ...

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //公用依赖包
    implementation project(':common_base')

    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
    //黄油刀
    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]

}
  • 配置文件,对项目中的第三库、app的版本等配置
/**
 *  全局统一配置文件
 */
ext {
    //true 每个业务Module可以单独开发
    //false 每个业务Module以lib的方式运行
    //修改之后需要Sync方可生效
    isModule = false
    
    //版本号
    versions = [
            applicationId           : "com.wss.amd",        //应用ID
            versionCode             : 1,                    //版本号
            versionName             : "1.0.0",              //版本名称

            compileSdkVersion       : 27,
            buildToolsVersion       : "27.0.3",
            minSdkVersion           : 17,
            targetSdkVersion        : 23,

            androidSupportSdkVersion: "27.1.1",
            constraintLayoutVersion : "1.1.1",
            runnerVersion           : "1.0.1",
            espressoVersion         : "3.0.1",
            junitVersion            : "4.12",
            annotationsVersion      : "24.0.0",

            multidexVersion         : "1.0.2",
            butterknifeVersion      : "8.4.0",
            arouterApiVersion       : "1.4.0",
            arouterCompilerVersion  : "1.2.1",
            arouterannotationVersion: "1.0.4",
            eventbusVersion         : "3.0.0",
            novateVersion           : "1.5.5",
            loggerVersion           : "2.2.0",
            fastjsonVersion         : "1.1.54",
            barlibraryVersion       : "2.3.0",
            picassoVersion          : "2.71828",
            bannerVersion           : "1.4.10",
            javaxVersion            : "1.2",
            lombokVersion           : "1.16.6",
            greendaoVersion         : "3.2.2",

    ]
    dependencies = ["appcompat_v7"        : "com.android.support:appcompat-v7:${versions["androidSupportSdkVersion"]}",
                    "constraint_layout"   : "com.android.support.constraint:constraint-layout:${versions["constraintLayoutVersion"]}",
                    "runner"              : "com.android.support.test:runner:${versions["runnerVersion"]}",
                    "espresso_core"       : "com.android.support.test.espresso:espresso-core:${versions["espressoVersion"]}",
                    "junit"               : "junit:junit:${versions["junitVersion"]}",
                    "support_annotations" : "com.android.support:support-annotations:${versions["annotationsVersion"]}",
                    "design"              : "com.android.support:design:${versions["androidSupportSdkVersion"]}",
                    "support-v4"          : "com.android.support:support-v4:${versions["androidSupportSdkVersion"]}",
                    "cardview-v7"         : "com.android.support:cardview-v7:${versions["androidSupportSdkVersion"]}",
                    "recyclerview-v7"     : "com.android.support:recyclerview-v7:${versions["androidSupportSdkVersion"]}",

                    //方法数超过65535解决方法64K MultiDex分包方法
                    "multidex"            : "com.android.support:multidex:${versions["multidexVersion"]}",

                    //路由
                    "arouter_api"         : "com.alibaba:arouter-api:${versions["arouterApiVersion"]}",
                    "arouter_compiler"    : "com.alibaba:arouter-compiler:${versions["arouterCompilerVersion"]}",
                    "arouter_annotation"  : "com.alibaba:arouter-annotation:${versions["arouterannotationVersion"]}",

                    //黄油刀
                    "butterknife_compiler": "com.jakewharton:butterknife-compiler:${versions["butterknifeVersion"]}",
                    "butterknife"         : "com.jakewharton:butterknife:${versions["butterknifeVersion"]}",

                    //事件订阅
                    "eventbus"            : "org.greenrobot:eventbus:${versions["eventbusVersion"]}",

                    //网络
                    "novate"              : "com.tamic.novate:novate:${versions["novateVersion"]}",

                    //日志
                    "logger"              : "com.orhanobut:logger:${versions["loggerVersion"]}",

                    //fastJson
                    "fastjson"            : "com.alibaba:fastjson:${versions["fastjsonVersion"]}.android",

                    //沉浸式状态栏
                    "barlibrary"          : "com.gyf.barlibrary:barlibrary:${versions["barlibraryVersion"]}",

                    //banner
                    "banner"              : "com.youth.banner:banner:${versions["bannerVersion"]}",

                    //图片加载
                    "picasso"             : "com.squareup.picasso:picasso:${versions["picassoVersion"]}",

                    //lombok
                    "lombokJavax"         : "javax.annotation:javax.annotation-api:${versions["javaxVersion"]}",
                    "lombok"              : "org.projectlombok:lombok:${versions["lombokVersion"]}",

                    //数据库
                    "greenDao"            : "org.greenrobot:greendao:${versions["greendaoVersion"]}",
    ]

}

最后别忘记在工程的中build.gradle引入该配置文件

apply from: "config.gradle"

修改isModule字段之后 需要Sysn才会生效

三.搭建过程中遇到的问题

1.Application、全局ContextActivity管理问题
  • 在功能组件即Demo中的common_base封装BaseApplication,在BaseApplication对第三方库初始化、全局Context的获取等操作。在BaseActivity中对Activity进行添加和移除的管理
//BaseApplicion
public class BaseApplication extends Application {
    ...
    //全局唯一的context
    private static BaseApplication application;

    //Activity管理器
    private ActivityManage activityManage;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        application = this;
        //MultiDex分包方法 必须最先初始化
        MultiDex.install(this);
    }
    public void onCreate() {
        super.onCreate();
        activityManage = new ActivityManage();
        initARouter();
        initLogger();
    }
  /**
     * 获取全局唯一上下文
     *
     * @return BaseApplication
     */
    public static BaseApplication getApplication() {
        return application;
    }
}

//BaseActivity
public abstract class BaseActivity extends Activity {  
    ...
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //加入Activity管理器
        BaseApplication.getApplication().getActivityManage().addActivity(this);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //将Activity从管理器移除
        BaseApplication.getApplication().getActivityManage().removeActivityty(this);
    }
}
2.AndroidManifest的管理

我们知道APP在打包的时候最后会把所有的AndroidManifest进行合并,所以每个业务组件的Activity只需要在各自模块的AndroidManifest中注册即可。如果业务组件需要独立运行,则需要单独配置一份AndroidManifest,在gradlesourceSets根据不同的模式加载不同的AndroidManifest文件。

业务组件Manifest.png

gradle配置

...
android {
   ...
    sourceSets {
        main {
            if (Boolean.valueOf(rootProject.ext.isModule)) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    //排除java/debug文件夹下的所有文件
                    exclude '*module'
                }
            }
        }
    }
}
...

注意:在配置Gradle的时候 manifest.srcFile... manifest 是小写的

其中集成模式加载的Manifest中不能设置Application和程序入口:

//集成模式下Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.wss.module.wan">

    <application>
        <activity android:name=".main.WanMainActivity" />
    </application>
</manifest>

//组件模式下Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.wss.module.wan">

    <application
        android:name=".common.WanApplication"
        android:allowBackup="true"
        android:label="@string/app_name"
        android:theme="@style/AdmTheme">

        <activity android:name=".main.WanMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

需要注意的是如果在组件开发模式下,组件的Applicaion必须继承自BaseApplicaion

3.不同组件之间的跳转

业务组件之间没有依赖,不能通过常规的Intent显示的进行跳转,这个时候就需要引入路由的概念

路由

利用阿里的ARouter对需要跳转的页面做配置
gradle配置

android {
   ...
       defaultConfig {
         ...
        //Arouter路由配置
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                includeCompileClasspath = true
            }
        }
    }
}
dependencies{
     ...
    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
}

目标页面配置

@Route(path = "/wan/WanMainActivity")
public class WanMainActivity extends ActionBarActivity<WanMainPresenter> implements IWanMainView, OnRcyItemClickListener {
      ...
}

跳转

...
   ARouter.getInstance()
          .build("/wan/WanMainActivity")
          .navigation();
...
4.不同组件之间通信

可以利用第三方 如EventBus对消息进行管理。在common_base组件中的Base类做了对消息的简单封装,子类只需要重写regEvent()返回true即可对事件的注册,重写onEventBus(Object)即可对事件的接收。

public abstract class BaseActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        if (regEvent()) {
            EventBus.getDefault().register(this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (regEvent()) {
            EventBus.getDefault().unregister(this);
        }
    }
    /**
     * 子类接收事件 重写该方法
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventBus(Object event) {
    }

    /**
     * 需要接收事件 重写该方法 并返回true
     */
    protected boolean regEvent() {
        return false;
    }
5.butterknife的问题

library中使用butterknife会存在找不到的问题。
推荐使用8.4.0版本,用R2代替R,onClick中使用if else不要使用switch case即可解决问题 。

public class HomeFragment extends BaseMvpFragment<HomePresenter> implements IHomeView, OnRcyItemClickListener {
   
    @BindView(R2.id.banner)
    Banner banner;

    @BindView(R2.id.recycle_view)
    RecyclerView recyclerView;  
    ...

    @OnClick({R2.id.tv_title, R2.id.btn_open})
    public void onClick(View v) {
        if (v.getId() == R.id.tv_title) {
            //do something

        } else if (v.getId() == R.id.btn_open) {
            //do something
        }
    }

}
6.资源文件冲突问题

目前没有比较好的约束方式,只能通过设置资源的前缀来防止资源文件冲突,然后在提交代码的时候对代码进行检查是否规范来控制。


资源文件命名.jpg

其中使用的MVP结构可以参考另一篇文章

最后放上Demo地址,共同学习,有什么不好的地方,欢迎大家指出!

参考文献
移动架构这么多,如何一次搞定所有
戏说移动江湖开发历程
模块化,组件化傻傻分不清?附带组件化福利
寄Android开发Gradle你需要知道的知识
解决组件化开发butterknife 在 library中使用的坑`

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

推荐阅读更多精彩内容

  • Android组件化项目地址:Android组件化项目AndroidModulePattern Android组件...
    半灬边灬天阅读 2,865评论 4 37
  • 前言 组件化是什么,是把一个功能完整的 App 或模块拆分成多个子模块, 表现在androidStudio项目工程...
    嘎啦果安卓兽阅读 8,481评论 6 121
  • 问题 在已经开发过几个项目的童鞋,如果这时需要重新开发一个新项目,是否需要自己重新搭建框架呢,还是从老项目中拷贝粘...
    8ba406212441阅读 42,831评论 84 381
  • 不怕跌倒,所以飞翔 组件化开发 参考资源 Android组件化方案 为什么要组件化开发 解决问题 实际业务变化非常...
    笔墨Android阅读 2,903评论 0 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707