文章目录

  • 写在前面
  • 谈谈模块化
    • 什么是模块化
    • 模块化的好处
    • 模块化的弊端
  • 聊聊组件化
    • 什么是组件化
    • 组件化的好处
  • 组件化实践
    • 组件化架构
    • 统一配置文件
    • 基础公共组件
    • 业务组件
  • 组件化必须要关注的几个问题
    • Application
    • applicationId管理
    • manifest管理
    • 资源名冲突问题
    • butterknife问题
    • 组件间跳转
    • 第三方sdk集成问题
  • 写在后面
  • 感谢

写在前面

学习容易,总结挺难;
输入容易,输出挺难;
学会容易,讲清挺难。
可不会总结,难以输出,无法讲清,算是真的学会吗?
——几句废话,警戒自己。

项目源码已经放在github上:https://github.com/ZuoHailong/AndroidModuleDemo (文后附的还有链接)

谈谈模块化

要聊组件化,惯例是要谈谈模块化的,毕竟它与组件化确实有一些相同点,在组件化的项目中它也会与组件化发生关联。

什么是模块化

模块化开发,是每个开发者都熟悉的。就是将常用的UI、网络请求、数据库操作、第三方库的使用等公共部分抽离封装成基础模块,或者将大的业务上拆分为多个小的业务模块,这些业务模块又依赖于公共基础模块的开发方式。
更宏观上,又会将这些不同的模块组合为一个整体,打包成一个完成的项目。

模块化的好处

模块化有哪些好处呢?

  • 复用
    首先,基础模块,可为业务模块所复用;
    其次,子业务模块,可为父业务模块,甚至不同的项目所复用。
  • 解耦
    降低模块间的耦合,避免出现一处代码修改,牵一发而动全身的尴尬局面。
  • 协同开发
    项目越来越大,团队人数越来越多,模块化开发可在尽量解耦的情况下,使不同的开发人员专注于自己负责的业务,同步开发,显著提供开发效率。
  • ……

模块化的弊端

那,模块化开发有没有什么弊端呢?
有。

任凭模块化做得多么好,还是跳不出组合在单一项目下的范围,项目规模越来越大,业务模块越来越多,团队人数越来越多,模块化开发渐渐出现了以下的问题:

  • 项目代码量越来越大,每次的编译速度越来越慢,哪怕几句代码的修改,都需要等待若干分钟等待编译运行查看执行结果,极大的降低了开发效率;
  • 业务模块越来越多,不可避免地产生越来越多且复杂的耦合,哪怕一次小的功能更新,也需要对修改代码耦合的模块进行充分测试;
  • 团队人数越来越多,却要求开发人员了解与之业务相关的每一个业务模块,防止出现此开发人员修改代码导致其他模块出现bug的情况,这个要求对于开发人员显然是不友好的;
  • ……

那怎样解决模块化开发的这些弊端呢?

当然是组件化!不然我下面扯个什么玩意儿?

聊聊组件化

组件化可以说是Android中级开发工程师必备技能了,能有效解决许多单一项目下开发出现的问题。并且我要强调的是,组件化真的不难,还没搞过的小伙伴不要怂。

什么是组件化

组件,顾名思义,组装的零件,术语上叫做软件单元,可用于组装在应用程序中。
从这个角度上看,组件化,要更关注可复用性、更注重关注点分离、功能单一、高内聚、粒度更小、是业务上能划分的最小单元,毕竟是“组装的零件”嘛!(观点来自——阿里巴巴·杨充,文后有感谢链接)
从这个角度上看,组件化的粒度,确实要比模块化的粒度更小。

看到这里,懵逼否?

组件化的概念,有点云里雾里,并且对上述观点,我认为大家还是要在各自的开发中见仁见智。

就我个人看法而言,要把组件化拆分到如此小的粒度,不可能,也没有必要。在组件化项目的实际开发中,组件化的粒度,是要比模块化的粒度更大的。

组件化的好处

首先要说的是,上述模块化的好处,组件化都有,不再赘述;上述模块化的弊端,组件化都给解决了,具体如下:

  • 组件,既可以作为library,又可以单独作为application,便于单独编译单独测试,大大的提高了编译和开发效率;
  • (业务)组件,可有自己独立的版本,业务线互不干扰,可单独编译、测试、打包、部署
  • 各业务线共有的公共模块开发为组件,作为依赖库供各业务线调用,减少重复代码编写,减少冗余,便于维护
  • 通过gradle配置文件,可对第三方库的引入进行统一管理,避免版本冲突,减少冗余库
  • 通过gradle配置文件,可对各组件实现library与application间便捷切换,实现项目的按需加载

组件化实践

首先要说明的是,下述是一个简单的不能再简单的组件化案例,只求帮助大家搭建起组件化的架构,不求实现什么具体的功能。

如果对组件化实现的丰富功能感兴趣,可参考文后的感谢链接,阅读大神们的项目源码。

九层之台,起于累土。我们还是先搭组件化的架构吧!

组件化架构

先上一张组件化项目整体架构图
其中的“业务组件”,既可以单独打包为apk,又可以作为library按需组合为综合一些的应用程序。

大多数开发者做组件化时面对的业务需求,都是上面这种情况。

我司的需求略有不同,不是将子业务组件组合为整体应用程序,而需要将已有项目拆分给不同业务体系使用,在不同业务体系下,项目的逻辑和代码会有区别,且版本不一致。

基于此,我搭建项目架构如下图所示,其中“m_moudle_main”是公司主要的、且逻辑和代码相同的业务组件,“b_moudle_north”和“b_moudle_south”是拆分出来的业务组件,管理各自私有的逻辑和代码,且版本有差别。
从Android工程看,结构如下图所示:


PS:取moudle名,手动加上“b_” “m_” “x_”这样的前缀,只是为了便于分辨组件层次。

统一配置文件

在项目根目录下,自建config.gradle文件,对项目进行全局统一配置,并对版本和依赖进行统一管理,源码如下

/** * 全局统一配置 */ext {    /**     * module开关统一声明在此处     * true:module作为application,可单独打包为apk     * false:module作为library,可作为宿主application的组件     */    isNorthModule = false    isSouthModule = false    /**     * 版本统一管理     */    versions = [            applicationId           : "com.hailong.amd",        //应用ID            versionCode             : 100,                    //版本号            versionName             : "1.0.0",              //版本名称            compileSdkVersion       : 28,            minSdkVersion           : 21,            targetSdkVersion        : 28,            androidSupportSdkVersion: "28.0.0",            constraintlayoutVersion : "1.1.3",            runnerVersion           : "1.1.0-alpha4",            espressoVersion         : "3.1.0-alpha4",            junitVersion            : "4.12",            annotationsVersion      : "28.0.0",            appcompatVersion        : "1.0.0-beta01",            designVersion           : "1.0.0-beta01",            multidexVersion         : "1.0.2",            butterknifeVersion      : "10.1.0",            arouterApiVersion       : "1.4.1",            arouterCompilerVersion  : "1.2.2",            arouterAnnotationVersion: "1.0.4"    ]    dependencies = [            "appcompat"           : "androidx.appcompat:appcompat:${versions["appcompatVersion"]}",            "constraintlayout"    : "androidx.constraintlayout:constraintlayout:${versions["constraintlayoutVersion"]}",            "runner"              : "androidx.test:runner:${versions["runnerVersion"]}",            "espresso_core"       : "androidx.test.espresso:espresso-core:${versions["espressoVersion"]}",            "junit"               : "junit:junit:${versions["junitVersion"]}",            //注释处理器            "support_annotations" : "com.android.support:support-annotations:${versions["annotationsVersion"]}",            "design"              : "com.google.android.material:material:${versions["designVersion"]}",            //方法数超过65535解决方法64K MultiDex分包方法            "multidex"            : "androidx.multidex:multidex:2.0.0",            //阿里路由            "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"         : "com.jakewharton:butterknife:${versions["butterknifeVersion"]}",            "butterknife_compiler": "com.jakewharton:butterknife-compiler:${versions["butterknifeVersion"]}"    ]}

然后在project的build.gradle中引入config.gradle文件

apply from: "config.gradle"

基础公共组件

基础公共组件common将一直作为library存在,所有业务组件都需要依赖common组件。common组件主要负责封装公共部分,如网络请求、数据存储、自定义控件、各种工具类等,以及对第三方库进行统一依赖。

下图为我的common组件的包结构图:

上文有言,common组件还负责对第三方库进行统一依赖,这样上层业务组件就不需要再对第三方库进行重复依赖了,其build.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"]    api rootProject.ext.dependencies["constraintlayout"]    api rootProject.ext.dependencies["junit"]    api rootProject.ext.dependencies["runner"]    api rootProject.ext.dependencies["espresso_core"]    //注释处理器,butterknife所必需    api rootProject.ext.dependencies["support_annotations"]    //MultiDex分包方法    api rootProject.ext.dependencies["multidex"]    //Material design    api rootProject.ext.dependencies["design"]    //黄油刀    api rootProject.ext.dependencies["butterknife"]    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]    //Arouter路由    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]    api rootProject.ext.dependencies["arouter_api"]    api rootProject.ext.dependencies["arouter_annotation"]}

业务组件

业务组件在集成模式下,是作为library存在,向上组合为整体性项目;在组件开发模式下,是作为application存在,可独立运行。以分支组件为例,其build.gradle源码如下:

if (Boolean.valueOf(rootProject.ext.isModule_North)) {    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(':x_module_common')    implementation project(':m_module_main')    //黄油刀    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]    //Arouter路由    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]}

至此,组件化架构的搭建基本完成。可还有几个问题,是组件化开发中所必需关注的,我们接着往下看。

组件化必须要关注的几个问题

Application

在common组件中有BaseAppliaction,提供全局唯一的context,上层业务组件在组件化模式下,均需继承于BaseAppliaction。

/** * Describe:基础Application,所有需要模块化开发的module都需要继承自此BaseApplication。 * Created by ZuoHailong on 2019/4/11. */public class BaseApplication extends Application {    //全局唯一的context    private static BaseApplication application;    @Override    protected void attachBaseContext(Context base) {        super.attachBaseContext(base);        application = this;        //MultiDexf分包初始化,必须最先初始化        MultiDex.install(this);    }    @Override    public void onCreate() {        super.onCreate();        initARouter();    }    /**     * 初始化路由     */    private void initARouter() {        if (BuildConfig.DEBUG) {            ARouter.openLog();  // 打印日志            ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)        }        ARouter.init(application);// 尽可能早,推荐在Application中初始化    }    /**     * 获取全局唯一上下文     *     * @return BaseApplication     */    public static BaseApplication getApplication() {        return application;    }

applicationId管理

可为不同组件设置不同的applicationId,也可缺省,在Android Studio中,默认的applicationId与包名一致。

组件的applicationId在其build.gradle文件的defaultConfig中进行配置:

if (Boolean.valueOf(rootProject.ext.isModule_North)) {    //组件模式下设置applicationId    applicationId "com.hailong.amd.north"}

manifest管理

组件在集成模式和组件化模式下,需要配置不同的manifest文件,因为在组件化模式下,程序入口Activity和自定义的Application是不可或缺的。

在组件的build.gradle文件的android中进行manifest的管理:

   /*    * java插件引入了一个概念叫做SourceSets,通过修改SourceSets中的属性,    * 可以指定哪些源文件(或文件夹下的源文件)要被编译,    * 哪些源文件要被排除。    * */    sourceSets {        main {            if (Boolean.valueOf(rootProject.ext.isModule_North)) {//apk                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'            } else {                manifest.srcFile 'src/main/AndroidManifest.xml'                java {                    //library模式下,排除java/debug文件夹下的所有文件                    exclude '*module'                }            }        }    }

资源名冲突问题

资源名冲突问题,相信大家多多少少都遇到过,以前最常见的就是第三方SDK导致的资源名冲突了。这个问题没有特别好的解决办法,只能通过设置资源名前缀 resourcePrefix 以及约束自己开发习惯进行解决。

资源名前缀 resourcePrefix ,是在project的build.gradle中进行设置的:

/** * 限定所有子类xml中的资源文件的前缀 * 注意:图片资源,限定失效,需要手动添加前缀 * */subprojects {    afterEvaluate {        android {            resourcePrefix "${project.name}_"        }    }}

这样设置完之后,string、style、color、dimens等中资源名,必须以设置的字符串为前缀,而layout、drawable文件夹下的shape他们的xml文件的命名,必须以设置的字符串为前缀,否则会报错提示。

另外,资源前缀的设置对图片的命名无法限定,建议大家约束自己的开发习惯,自觉加上前缀。

建议:
将color、shape、style这些放在基础库组件中去,这些资源不会太多,且复用性极高,所有业务组件又都会依赖基础库组件。

butterknife问题

有博主在去年的博文中建议大家选用butterknife 8.4.0版本,我进行组件化构建时,目前的最新版10.1.0用起来是没有问题的,大家可放心将butterknife升级到最新版。

butterknife存在是问题是控件ID找不到,只要将R替换为R2即可解决问题。需要注意的是,在如下代码示例外的位置,不要这样做,保持使用R即可,如 setContentView(R.layout.b_module_north_activity_splash)

/** * Describe: * Created by ZuoHailong on 2019/4/19. */public class SplashActivity extends BaseActivity {    @BindView(R2.id.btn_toMain)    Button btnToMain;    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.b_module_north_activity_splash);        ButterKnife.bind(this);    }    ……    @OnClick(R2.id.btn_toMain)    public void onViewClicked() {    }}

另外要注意的是,每一个使用butterknife的组件,在其 build.gradle 的 dependencies 都要配置注解处理器处理其 compiler 库

apply plugin: 'com.jakewharton.butterknife'……dependencies {    ……    //黄油刀    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]}

组件间跳转

业务组件间不存在依赖关系,不可以通过Intent进行显式跳转,是要借助于路由的,我使用的是阿里的开源框架ARouter。

我在案例中只使用了ARouter的基础的页面跳转功能,更复杂的诸如携带参数跳转、声明拦截器等功能的使用方法,大家可到Github上查看其使用文档。

在每一个需要用到ARouter的组件的build.gradle文件中对其进行配置:

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

跳转目标页面配置:

@Route(path = "/main/MainActivity")public class MainActivity extends BaseActivity {   ……}

跳转发起页面的发起调用:

...   ARouter.getInstance()          .build("/main/MainActivity")          .navigation();...

第三方sdk集成问题

项目不可避免的要使用第三方sdk,如友盟分享、高德地图、腾讯 bugly 等,都需要在相应第三方的开发者中心使用包名、applicationId注册,获取相应的 appkey 、appsecret等。

那么,在组件化开发中,到底应该使用哪个组件的包名、applicationId 到第三方平台进行注册呢?

我的想法是使用基础库的包名、applicationId 进行注册,然后将相应的第三方sdk的功能封装为功能组件,供上层业务组件进行调用。

但也存在一个问题,即在第三方的管理平台上,将无法区分相应统计信息到底属于哪一个组件化app。

所以还是要根据业务要求、统计要求自己选择了。

写在后面

组件化优势多多,本文标题上说不可自拔,快感来的最快的,当属大大提升了编译速度了。


项目源码已经放在github上:https://github.com/ZuoHailong/AndroidModuleDemo

感谢

鸿洋大神:wanAndroid

吴蜀黍:Android组件化框架搭建

杨充:Android组件化开发实践和案例分享

更多相关文章

  1. 没有一行代码,「2020 新冠肺炎记忆」这个项目却登上了 GitHub 中
  2. 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
  3. Eclipse中Android公共库的正确建立及调用方法
  4. Android自动化测试框架Robotium学习笔记
  5. 一文带你了解Android(安卓)Jetpack
  6. Android项目使用私有maven仓库配置
  7. Android开发周报:Android之父离开谷歌自立门户
  8. Flutter混合开发踩坑指北
  9. Android第三方应用集成到Android系统的解决方法

随机推荐

  1. 全国高校绿色计算大赛 预赛第一阶段(Pytho
  2. python在文本开头插入一行
  3. Python爬虫(一):基本概念
  4. Python 学习笔记二
  5. python 报错——Python TypeError: 'modu
  6. python获取外网IP并发邮件
  7. 在Python中,如何在使用WPF的程序中自动控
  8. CentOS下实现Flask + Virtualenv + uWSGI
  9. PySide-QtWebKit: CSS font-family没有效
  10. python学习笔记10(函数一): 函数使用、调用