Android组件化框架搭建

作者: 吴蜀黍 | 来源:发表于2018-10-19 16:52 被阅读25次

    背景

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

    一.基础搭建

    先来一张整个项目构思图


    项目构思图

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


    项目结构图

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

    • 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模块无论在什么情况下都是以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"
    ...
    

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

    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'
                    }
                }
            }
        }
    }
    ...
    

    其中集成模式加载的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.不同组件之间的跳转

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

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

    目标页面配置

    @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
    最后放上Demo地址,共同学习,有什么不好的地方,欢迎大家指出!

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

    相关文章

      网友评论

      • Jafir:你好 我看你的框架里使用了butterknife,请问它在library中需要转为R2,独立module下又是正常R,你们是做了什么转换吗?
        吴蜀黍:@Jafir 不用转换 使用R2即可 library和application都可以使用
      • 叨码:如果涉及到登录逻辑呢。单独的module作为application的话 如何处理登录 放到壳里去处理么
        吴蜀黍:可以在子module中模拟登录,不外呼就是调用几个接口的事儿。
      • e838b092554b:楼主,我想问下 不同组件之间的数据怎么传递?
        e838b092554b:@吴蜀黍 我想的也是这样,谢谢
        吴蜀黍:基本类型跟Intent跳转传值一样,bean的话需要把需要传递的bean类下沉到common模块下。
        e838b092554b:好像不同组件要用同一个 bean 只能放在common下了
      • 我是龙俊:我把isModule改成ture 单独去运行每个module后 gradle总是报错Could not get unknown property 'packageForR' for task ':module_wan_android:processDebugResources' of type com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask.

        参考https://blog.csdn.net/qq_31852701/article/details/80312719类似博客还是没成功 我用的as是3.2 gradle version 4.6 android gralde plugin verison 3.2.0 跟版本有关系吗
        我是龙俊:@吴蜀黍 我升级butterknife版本后 没有找不到r2.id
        吴蜀黍:我大概看了下问题应该是butterknife注解框架的问题,我目前项目中使用的AS是3.0.1还没有升级带最新的版本,你可以升级一下Demo中butterknife 的版本去看下对应的配置
        吴蜀黍:@我是龙俊 目前我没有发现这个问题 我自己用的as3.0.1等我升级了看一下
      • 蜗蜗牛牛:写的很好,关于路由我有个想法,不知道可行不:
        1.common_base模块对应每一个模块写一个接口,接口中定义各自模块能够提供给其他模块的功能,包括页面跳转,开启服务等等
        2.每个模块对应common_base中的接口,实现一个管理类,这个管理类A有个路由地址
        3.common_base写个路由管理类B,在这个类中可以获取所有的模块的管理类A
        4.模块之间相互调用时,就可以直接通过管理类B来实现,不需要再去其他模块具体的Activity是什么地址.也不用再去维护那么多的路由地址,每个模块只有管理类需要一个地址就可以了

        我也刚刚学组件化和路由模块,实际项目中没有用过,希望大佬指导下,以上方案是否可行
        吴蜀黍:没有具体试过也不好妄下结论,你可以按照你说的去写个Demo试下。
        关于你说的第三点,肯定是实现不了的因为所有的业务Module依赖于comon_base,所以common_base相当于子Module的父亲,子类可以集成父类的东西,但是父亲是拿不到子Module的东西的。
      • 听雨如许:大佬,希望可以不断完善哦,很实用很详细的教程
        吴蜀黍::joy: 踩在巨人的肩膀上共同学习
      • 我头疼了哈:可不可以每个tab当成一个module 独立出来呢
        我头疼了哈:@吴蜀黍 单独运行某个组件会崩溃
        吴蜀黍:代码已提交
        吴蜀黍:@我头疼了哈 可以 我最新的demo就是 每个tab一个module 等我明天把代码提交了 你看下 ARouter可以通过path 获取activity和fragment
      • sinbara:为什么我把isModule改成ture 工具栏上方的run还是只可以选择app运行 不能运行module_wan_android
        吴蜀黍:@sinbara 我已经添加说明了, 如果你在github上拉取过代码了 你可以仔细看一遍我写的注释,这个我在config.gradle文件中已经有标注说明了
        吴蜀黍:@sinbara app是壳子 永远都是application 你改了之后 需要同步一下才会有 稍后我加上去
        sinbara:lz app下的gradle应该加上下面代码吧
        if (Boolean.valueOf(rootProject.ext.isModule)) {
        apply plugin: 'com.android.library'
        } else {
        apply plugin: 'com.android.application'
        }
      • 小周爱吃瓜:大佬,有GitHub的demo吗
        吴蜀黍:@zb666 :stuck_out_tongue_closed_eyes:
        小周爱吃瓜:@吴蜀黍 眼拙:sweat:
        吴蜀黍:最后放上Demo地址,共同学习,有什么不好的地方,欢迎大家指出!

        文章底部有连接的

      本文标题:Android组件化框架搭建

      本文链接:https://www.haomeiwen.com/subject/lotuzftx.html