前言

随着项目业务不断增多,功能越来越复杂,各个业务代码耦合越来越多,代码量急剧增加,难以高效开发与管理

  1. 各模块耦合严重,相互引用实现
  2. 修改旧模块,难以分析对其他模块影响
  3. 新开业务,必须在整个工程中引入,多个开发人员存在同步问题
  4. 难以做单元测试,测试某个模块,需要在整个工程中测试,编译时间过长

    模块

    一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,比如登录模块首页模块等等。

    组件化

    组件指的是单一的功能组件,如登录组件视频组件支付组件 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用。可以说往往一个模块包含了一个或多个组件。
    根据业务和职责将整个工程拆分成各个组件,然后以某种方式解决组件间的依赖关系,做到单一职责、高内聚、低耦合。
    最终效果:每个业务组件都是一个微型App,SDK。

    组件化的优势

    组件化基于可重用的目的,将应用拆分成多个独立组件,以减少耦合:
  • 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
  • 解耦:通过关注点分离的形式,将App分离成多个模块,每个模块都是一个组件。
  • 提高开发效率:多人开发中,每个组件模块由单人负责,降低了开发之间沟通的成本,减少因代码风格不一而产生的相互影响。
  • 代码复用:类似我们引用的第三方库,可以将基础组件或功能组件剥离。在新项目微调或直接使用。

    组件化分层架构

    组件依赖关系是上层依赖下层,修改频率是上层高于下层。先上一张图:
    image.png

    基础组件

    基础公共模块,最底层的库:

  • 封装公用的基础组件;

  • 网络访问框架、图片加载框架等主流的第三方库;
  • 各种第三方SDK。

    common 组件

  • 支撑业务组件、功能组件的基础(BaseActivity/BaseFragment等基础能力;

  • 依赖基础组件层;
  • 业务组件、功能组件所需的基础能力只需要依赖common组件即可获得。

    功能组件

  • 依赖基础组件层;

  • 对一些公用的功能业务进行封装与实现;
  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

    业务组件

  • 可直接依赖基础组件层;同时也能依赖公用的一些功能组件;

  • 各组件之间不存在依赖关系,通过路由进行通信;
  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

    主工程(app)

  • 只依赖各业务组件;

  • 除了一些全局的配置和主Activity之外,不包含任何业务代码,是应用的入口;

    要解决的问题

    1. gradle 统一配置

    这里比较合适的是 buildSrc 和includeBuild,相关使用方式可以参考版本统一依赖管理

    2. 组件独立调试

    一个组件可以根据需要,既可以是一个单独的module,可以单独调试。又可以是一个library,可以被其他组件依赖。
    如何让组件在这两种调试模式之间自动转换呢? 手动修改组件的 gralde 文件,切换 Application 和 library ?如果项目只有两三个组件那么是可行的,但在大型项目中可能会有十几个业务组件,一个个手动修改显得费力笨拙。
    我们知道用AndroidStudio创建一个Android项目后,会在根目录中生成一个gradle.properties文件。在这个文件定义的常量,可以被任何一个build.gradle读取。 所以我们可以在gradle.properties中定义一个常量值 isModule,true为即独立调试;false为集成调试。然后在业务组件的build.gradle中读取 isModule,设置成对应的插件即可。代码如下:
    1. //gradle.properties
    2. #组件独立调试开关, 每次更改值后要同步工程
    3. isModule = false
    1. //build.gradle
    2. //注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
    3. if (isModule.toBoolean()){
    4. apply plugin: 'com.android.application'
    5. }else {
    6. apply plugin: 'com.android.library'
    7. }
  1. Application
    在 common 组件中有 BaseAppliaction,提供全局唯一的 context,上层业务组件在组件化模式下,均需继承于 BaseAppliaction。

    1. /**
    2. * 基础 Application,所有需要模块化开发的 module 都需要继承自此 BaseApplication。
    3. */
    4. public class BaseApplication extends Application {
    5. //全局唯一的context
    6. private static BaseApplication application;
    7. @Override
    8. protected void attachBaseContext(Context base) {
    9. super.attachBaseContext(base);
    10. application = this;
    11. //MultiDexf分包初始化,必须最先初始化
    12. MultiDex.install(this);
    13. }
    14. @Override
    15. public void onCreate() {
    16. super.onCreate();
    17. initARouter();
    18. }
    19. /**
    20. * 初始化路由
    21. */
    22. private void initARouter() {
    23. if (BuildConfig.DEBUG) {
    24. ARouter.openLog(); // 打印日志
    25. ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
    26. }
    27. ARouter.init(application);// 尽可能早,推荐在Application中初始化
    28. }
    29. /**
    30. * 获取全局唯一上下文
    31. *
    32. * @return BaseApplication
    33. */
    34. public static BaseApplication getApplication() {
    35. return application;
    36. }

    4. Manifest.xml管理

  • 一个 APP 是只有一个 ApplicationId ,所以在单独调试集成调试组件的 ApplicationId 应该是不同的。
  • 单独调试时也是需要有一个启动页,当集成调试时主工程和组件的AndroidManifest文件合并会产生多个启动页。

根据上面动态配制插件的经验,我们也需要在build.gradle中动态配制ApplicationId 和 AndroidManifest 文件。
image.png

5. 资源文件重名问题

资源文件命名冲突,看过很多解决方法,最常见的就是第三方 sdk 导致的资源名冲突了。
这个问题没有特别好的解决办法,只能通过设置资源名前缀 resourcePrefix 以及约束自己开发习惯进行解决。
资源名前缀 resourcePrefix ,是在 Project 的 build.gradle 中进行设置的:

  1. /**
  2. * 限定所有子类xml中的资源文件的前缀
  3. * 注意:图片资源,限定失效,需要手动添加前缀
  4. * */
  5. subprojects {
  6. afterEvaluate {
  7. android {
  8. resourcePrefix "${project.name}_"
  9. }
  10. }
  11. }

这样设置完之后,string、style、color、dimens 等中资源名,必须以设置的字符串为前缀,而 layout、drawable 文件夹下的 shape 的 xml 文件的命名,必须以设置的字符串为前缀,否则会报错提示。
另外,资源前缀的设置对图片的命名无法限定,建议大家约束自己的开发习惯,自觉加上前缀。
上面的方法是对整个项目设置,如果想对单个module进行设置,在android 中设置 resourcePrefix 属性,前缀字符串根据实际情况自定义。设置方式如下:

  1. android {
  2. compileSdkVersion 28
  3. resourcePrefix "app_"
  4. }

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

6. 界面跳转

前面说到,组件化的核心就是解耦,所以组件间是不能有依赖的,那么如何实现组件间的页面跳转呢?
例如 在首页模块 点击 购物车按钮 需要跳转到 购物车模块的购物车页面,两个模块之间没有依赖,也就说不能直接使用 显示启动 来打开购物车Activity,那么隐式启动呢? 隐式启动是可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。这里我们采用业界通用的方式—路由
比较著名的路由框架 有阿里的ARouter、美团的WMRouter,它们原理基本是一致的。
这里我们采用使用更广泛的ARouter:“一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦”。使用方式可以参考官方文档。

7. 组件间通信

可以考虑使用Eventbus,或者在公共组件使用接口。

参考

Android 组件化最佳实践
Android 手把手带你搭建一个组件化项目架构
Android 组件化,从入门到不可自拔
“终于懂了” 系列:Android组件化,全面掌握! | 掘金技术征文-双节特别篇
Android 项目组件化详细实施方案