阅读 9862

“终于懂了” 系列:Android组件化,全面掌握! | 掘金技术征文-双节特别篇

一、背景

随着项目逐渐扩展,业务功能越来越多,代码量越来越多,开发人员数量也越来越多。此过程中,你是否有过以下烦恼?

  1. 项目模块多且复杂,编译一次要5分钟甚至10分钟?太慢不能忍?
  2. 改了一行代码 或只调了一点UI,就要run整个项目,再忍受一次10分钟?
  3. 合代码经常发生冲突?很烦?
  4. 被人偷偷改了自己模块的代码?很不爽?
  5. 做一个需求,发现还要去改动很多别人模块的代码?
  6. 别的模块已实现的类似功能,自己要用只能去复制一份代码再改改?
  7. “这个不是我负责的,我不管”,代码责任范围不明确?
  8. 只做了一个模块的功能,但改动点很多,所以要完整回归测试?
  9. 做了个需求,但不知不觉导致其他模块出现bug?

如果有这些烦恼,说明你的项目需要进行 组件化 了。

上半年,我所在项目进行了大重构,也完成了组件化改造。所以终于学习实践了这样一个“高端知识”,也看了一些文章,于是就有了这篇文章来作为总结和分享~

二、组件化的理解

2.1 模块化

在介绍组件化之前,先说说模块化。我们知道在Android Studio中,新建工程默认有一个App module,然后还可以通过File->New->New Module 新建module。那么这里的“module” 实际和我们说的“模块”基本是一个概念了。也就是说,原本一个 App模块 承载了所有的功能,而模块化就是拆分成多个模块放在不同的Module里面,每个功能的代码都在自己所属的 module 中添加。

已京东为例,大致可以分为 “首页”、“分类”、“发现”、“购物车”、“我的”、“商品详情” 六个模块。 京东 项目结构如下: 京东模块划分

这是一般项目都会采用的结构。另外通常还会有一个通用基础模块module_common,提供BaseActivity/BaseFragment、图片加载、网络请求等基础能力,然后每个业务模块都会依赖这个基础模块。 那么业务模块之间有没有依赖呢?很显然是有的。例如 “首页”、“分类”、“发现”、“购物车”、“我的”,都是需要跳转到“商品详情” 的,必然是依赖“商品详情” ;而“商品详情”是需要能添加到“购物车”能力的;而“首页”点击搜索显然是“分类”中的搜索功能。所以这些模块之间存在复杂的依赖关系

模块间存在复杂依赖关系 模块化 在各个业务功能比较独立的情况下是比较合理的,但多个模块中肯定会有页面跳转数据传递方法调用 等情况,所以必然存在以上这种依赖关系,即模块间有着高耦合度。 高耦合度 加上 代码量大,就极易出现上面提到的那些问题了,严重影响了团队的开发效率及质量。

为了 解决模块间的高耦合度问题,就要进行组件化了。

2.2 组件化介绍 — 优势及架构

组件化去除模块间的耦合,使得每个业务模块可以独立当做App存在,对于其他模块没有直接的依赖关系。 此时业务模块就成为了业务组件

而除了业务组件,还有抽离出来的业务基础组件,是提供给业务组件使用,但不是独立的业务,例如分享组件、广告组件;还有基础组件,即单独的基础功能,与业务无关,例如 图片加载、网络请求等。这些后面会详细说明。

组件化带来的好处 就显而易见了:

  1. 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
  2. 提高协作效率:解耦 使得组件之间 彼此互不打扰,组件内部代码相关性极高。 团队中每个人有自己的责任组件,不会影响其他组件;降低团队成员熟悉项目的成本,只需熟悉责任组件即可;对测试来说,只需重点测试改动的组件,而不是全盘回归测试。
  3. 功能重用:组件 类似我们引用的第三方库,只需维护好每个组件,一建引用集成即可。业务组件可上可下,灵活多变;而基础组件,为新业务随时集成提供了基础,减少重复开发和维护工作量。

下图是我们期望的组件化架构

组件化架构

  1. 组件依赖关系是上层依赖下层,修改频率是上层高于下层。
  2. 基础组件是通用基础能力,修改频率极低,作为SDK可共公司所有项目集成使用。
  3. common组件,作为支撑业务组件、业务基础组件的基础(BaseActivity/BaseFragment等基础能力),同时依赖所有的基础组件,提供多数业务组件需要的基本功能,并且统一了基础组件的版本号。所以 业务组件、业务基础组件 所需的基础能力只需要依赖common组件即可获得。
  4. 业务组件业务基础组件,都依赖common组件。但业务组件之间不存在依赖关系,业务基础组件之间不存在依赖关系。而 业务组件 是依赖所需的业务基础组件的,例如几乎所有业务组件都会依赖广告组件 来展示Banner广告、弹窗广告等。
  5. 最上层则是主工程,即所谓的“壳工程”,主要是集成所有的业务组件、提供Application唯一实现、gradle、manifest配置,整合成完备的App。

2.3 组件化开发的问题点

我们了解了组件化的概念、优点及架构特点,那么要想实施组件化,首先要搞清楚 要解决问题点有哪些?

核心问题是 业务组件去耦合。那么存在哪些耦合的情况呢?前面有提到过,页面跳转、方法调用、事件通知。 而基础组件、业务基础组件,不存在耦合的问题,所以只需要抽离封装成库即可。 所以针对业务组件有以下问题:

  1. 业务组件,如何实现单独运行调试?
  2. 业务组件间 没有依赖,如何实现页面的跳转?
  3. 业务组件间 没有依赖,如何实现组件间通信/方法调用?
  4. 业务组件间 没有依赖,如何获取fragment实例?
  5. 业务组件不能反向依赖壳工程,如何获取Application实例、如何获取Application onCreate()回调(用于任务初始化)?

下面就来看看如何解决这些问题。

三、组件独立调试

每个 业务组件 都是一个完整的整体,可以当做独立的App,需要满足单独运行及调试的要求,这样可以提升编译速度提高效率。

如何做到组件独立调试呢?有两种方案:

  1. 单工程方案,组件以module形式存在,动态配置组件的工程类型;
  2. 多工程方案,业务组件以library module形式存在于独立的工程,且只有这一个library module。

3.1 单工程方案

3.1.1 动态配置组件工程类型

单工程模式,整个项目只有一个工程,它包含:App module 加上各个业务组件module,就是所有的代码,这就是单工程模式。 如何做到组件单独调试呢?

我们知道,在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来配置不同的module类型。

  • Application 插件,id: com.android.application
  • Library 插件,id: com.android.library

区别比较简单, App 插件来配置一个 Android App 工程,项目构建后输出一个 APK 安装包,Library 插件来配置一个 Android Library 工程,构建后输出 ARR 包。

app module配置的application插件 业务module配置的library插件

显然我们的 App module配置的就是Application 插件,业务组件module 配置的是 Library 插件。想要实现 业务组件的独立调试,这就需要把配置改为 Application 插件;而独立开发调试完成后,又需要变回Library 插件进行集成调试

如何让组件在这两种调试模式之间自动转换呢? 手动修改组件的 gralde 文件,切换 Application 和 library ?如果项目只有两三个组件那么是可行的,但在大型项目中可能会有十几个业务组件,一个个手动修改显得费力笨拙。

我们知道用AndroidStudio创建一个Android项目后,会在根目录中生成一个gradle.properties文件。在这个文件定义的常量,可以被任何一个build.gradle读取。 所以我们可以在gradle.properties中定义一个常量值 isModule,true为即独立调试;false为集成调试。然后在业务组件的build.gradle中读取 isModule,设置成对应的插件即可。代码如下:

//gradle.properties
#组件独立调试开关, 每次更改值后要同步工程
isModule = false
复制代码
//build.gradle
//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if (isModule.toBoolean()){
    apply plugin: 'com.android.application'
}else {
    apply plugin: 'com.android.library'
}
复制代码

3.1.2 动态配置ApplicationId 和 AndroidManifest

我们知道一个 App 是需要一个 ApplicationId的 ,而组件在独立调试时也是一个App,所以也需要一个 ApplicationId,集成调试时组件是不需要ApplicationId的;另外一个 APP 也只有一个启动页, 而组件在独立调试时也需要一个启动页,在集成调试时就不需要了。所以ApplicationId、AndroidManifest也是需要 isModule 来进行配置的。

//build.gradle (module_cart)
android {
...
    defaultConfig {
...
        if (isModule.toBoolean()) {
            // 独立调试时添加 applicationId ,集成调试时移除
            applicationId "com.hfy.componentlearning.cart"
        }
...
    }

    sourceSets {
        main {
            // 独立调试与集成调试时使用不同的 AndroidManifest.xml 文件
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/moduleManifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
...
}
复制代码

可见也是使用isModule分别设置applicationId、AndroidManifest。其中独立调试的AndroidManifest是新建于目录moduleManifest,使用 manifest.srcFile 即可指定两种调试模式的AndroidManifest文件路径。 独立调试的manifest moduleManifest中新建的manifest文件 指定了Application、启动activity:

//moduleManifest/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfy.module_cart" >
    <application android:name=".CartApplication"
        android:allowBackup="true"
        android:label="Cart"
        android:theme="@style/Theme.AppCompat">
        <activity android:name=".CartActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
复制代码

原本自动生成的manifest,未指定Application、启动activity:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfy.module_cart">
    <application>
        <activity android:name=".CartActivity"></activity>
    </application>
</manifest>
复制代码

独立调试、集成调试 ,分别使用“assembleDebug”构建结果如下: 独立调试、集成调试 构建对应的arr、apk

3.2 多工程方案

3.2.1 方案概述

多工程方案,业务组件以library module形式存在于独立的工程。独立工程 自然就可以独立调试了,不再需要进行上面那些配置了。

例如,购物车组件 就是 新建的工程Cart 的 module_cart模块,业务代码就写在module_cart中即可。app模块是依赖module_cart。app模块只是一个组件的入口,或者是一些demo测试代码。 组件存在于独立工程 那么当所有业务组件都拆分成独立组件时,原本的工程就变成一个只有app模块的壳工程了,壳工程就是用来集成所有业务组件的。

3.2.1 maven引用组件

那么如何进行集成调试呢?使用maven引用组件:1、发布组件的arr包 到公司的maven仓库,2、然后在壳工程中就使用implemention依赖就可以了,和使用第三方库一毛一样。另外arr包 分为 快照版本(SNAPSHOT) 和 正(Realease)式版本,快照版本是开发阶段调试使用,正式版本是正式发版使用。具体如下:

首先,在module_cart模块中新建maven_push.gradle文件,和build.gradle同级目录

apply plugin: 'maven'

configurations {
    deployerJars
}

repositories {
    mavenCentral()
}

//上传到Maven仓库的task
uploadArchives {
    repositories {
        mavenDeployer {
            pom.version = '1.0.0' // 版本号
            pom.artifactId = 'cart' // 项目名称(通常为类库模块名称,也可以任意)
            pom.groupId = 'com.hfy.cart' // 唯一标识(通常为模块包名,也可以任意)

            //指定快照版本 maven仓库url, todo 请改为自己的maven服务器地址、账号密码
            snapshotRepository(url: 'http://xxx/maven-snapshots/') {
                authentication(userName: '***', password: '***')
            }
            //指定正式版本 maven仓库url, todo 请改为自己的maven服务器地址、账号密码
            repository(url: 'http://xxx/maven-releases/') {
                authentication(userName: '***', password: '***')
            }
        }
    }
}

// type显示指定任务类型或任务, 这里指定要执行Javadoc这个task,这个task在gradle中已经定义
task androidJavadocs(type: Javadoc) {
    // 设置源码所在的位置
    source = android.sourceSets.main.java.sourceFiles
}

// 生成javadoc.jar
task androidJavadocsJar(type: Jar) {
    // 指定文档名称
    classifier = 'javadoc'
    from androidJavadocs.destinationDir
}

// 打包main目录下代码和资源的task,生成sources.jar
task androidSourcesJar(type: Jar) {
    classifier = 'sources'
    from android.sourceSets.main.java.sourceFiles
}

//配置需要上传到maven仓库的文件
artifacts {
    archives androidSourcesJar
    archives androidJavadocsJar
}
复制代码

maven_push.gradle主要就是发布组件ARR的配置:ARR的版本号、名称、maven仓地址账号等。

然后,再build.gradle中引用:

//build.gradle
apply from: 'maven_push.gradle'
复制代码

接着,点击Sync后,点击Gradle任务uploadArchives,即可打包并发布arr到maven仓。

打包并发布arr到maven 最后,壳工程要引用组件ARR,需要先在壳工程的根目录下build.gradle中添加maven仓库地址:

allprojects {
    repositories {
        google()
        jcenter()
        //私有服务器仓库地址
        maven {
            url 'http://xxx'
        }
    }
}
复制代码

接着在app的build.gradle中添加依赖即可:

dependencies {
    ...
    implementation 'com.hfy.cart:cart:1.0.0'
    //以及其他业务组件
}
复制代码

可见,多工程方案 和我们平时使用第三方库是一样的,只是我们把组件ARR发布到公司的私有maven仓而已。

实际上,我个人比较建议 使用多工程方案的。

  • 单工程方案没法做到代码权限管控,也不能做到开发人员职责划分明确,每个开发人员都可以对任意的组件进行修改,显然还是会造成混乱。
  • 多工程把每个组件都分割成单独的工程,代码权限可以明确管控。集成测试时,通过maven引用来集成即可。并且业务组件和业务基础组件也可以 和 基础组件一样,可以给公司其他项目复用。

注意,我在Demo里 使用的是多工程方案,并且是 把ARR发到JitPack仓,这样是为了演示方便,和发到公司私有maven仓是一个意思。 1、需要根目录下build.gradle中添加JitPack仓地址:maven { url 'https://jitpack.io' } ; 2、JitPack是自定义的Maven仓库,不过它的流程极度简化,只需要输入Github项目地址就可发布项目。

四、页面跳转

4.1 方案—ARouter

前面说到,组件化的核心就是解耦,所以组件间是不能有依赖的,那么如何实现组件间的页面跳转呢?

例如 在首页模块 点击 购物车按钮 需要跳转到 购物车模块的购物车页面,两个模块之间没有依赖,也就说不能直接使用 显示启动 来打开购物车Activity,那么隐式启动呢? 隐式启动是可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。这里我们采用业界通用的方式—路由

比较著名的路由框架 有阿里的ARouter、美团的WMRouter,它们原理基本是一致的。

这里我们采用使用更广泛的ARouter:“一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦”。

4.2 ARouter实现路由跳转

前面提到,所有的业务组件都依赖了 Common 组件,所以我们在 Common 组件中使用关键字**“api”**添加的依赖,业务组件都能访问。 我们要使用 ARouter 进行界面跳转,需要Common 组件添加 Arouter 的依赖(另外,其它组件共同依赖的库也要都放到 Common 中统一依赖)。

4.2.1 引入依赖

因为ARouter比较特殊,“arouter-compiler ” 的annotationProcessor依赖 需要所有使用到 ARouter 的组件中都单独添加,不然无法在 apt 中生成索引文件,就无法跳转成功。并且在每个使用到 ARouter 的组件的 build.gradle 文件中,其 android{} 中的 javaCompileOptions 中也需要添加特定配置。然后壳工程需要依赖业务组件。如下所示:

//common组件的build.gradle
dependencies {
    ...
    api 'com.alibaba:arouter-api:1.4.0'
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    //业务组件、业务基础组件 共同依赖的库(网络库、图片库等)都写在这里~
}
复制代码
//业务组件的build.gradle
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
...
}
dependencies {
...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'//业务组件依赖common组件
}
复制代码
//壳工程app module的build.gradle
dependencies {
    ...
    //这里没有使用私有maven仓,而是发到JitPack仓,一样的意思~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang:Cart:1.0.1' //依赖购物车组件
    implementation 'com.github.hufeiyang:HomePage:1.0.2' //依赖首页组件

    //壳工程内 也需要依赖Common组件,因为需要初始化ARouter
    implementation 'com.github.hufeiyang:Common:1.0.0'
}
复制代码

4.2.2 初始化

依赖完了,先要对ARouter初始化,需要在Application内完成:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // 这两行必须写在init之前,否则这些配置在init过程中将无效
        if (BuildConfig.DEBUG) {
            // 打印日志
            ARouter.openLog();
            // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
            ARouter.openDebug();
        }
        // 尽可能早,推荐在Application中初始化
        ARouter.init(this);
    }
}
复制代码

4.2.3 路由跳转

好了,准备工作都完成了。并且知道 首页组件是没有依赖购物车组件的,下面就来实现前面提到的 首页组件 无依赖 跳转到 购物车组件页面

而使用ARouter进行简单路由跳转,只有两步:添加注解路径、通过路径路由跳转。

1、在支持路由的页面上添加注解@Route(path = "/xx/xx"),路径需要注意的是至少需要有两级,/xx/xx。这里就是购物车组件的CartActivity:

@Route(path = "/cart/cartActivity")
public class CartActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cart);
    }
}
复制代码

2、然后在首页组件的HomeActivity 发起路由操作—点击按钮跳转到购物车,调用ARouter.getInstance().build("/xx/xx").navigation()即可:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通过路由跳转到 购物车组件的购物车页面(但没有依赖购物车组件)
                ARouter.getInstance()
                        .build("/cart/cartActivity")
                        .withString("key1","value1")//携带参数1
                        .withString("key2","value2")//携带参数2
                        .navigation();
            }
        });
    }
}
复制代码

另外,注意在HomeActivity上添加了注解和路径,这是为了壳工程的启动页中直接打开首页。还看到路由跳转可以像startActivity一样待参数。

最后,壳工程的启动页中 通过路由打开首页(当然这里也可以用startActivity(),毕竟壳工程依赖了首页组件):

//启动页
public class SplashActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //通过路由直接打开home组件的HomeActivity,
        ARouter.getInstance().build("/homepage/homeActivity").navigation();
        finish();
    }
}
复制代码

我们run壳工程 最后看下效果: 无依赖首页跳购物车 到这里,组件间页面跳转的问题也解决了。

五、组件间通信

组件间没有依赖,又如何进行通信呢?

例如,首页需要展示购物车中商品的数量,而查询购物车中商品数量 这个能力是购物车组件内部的,这咋办呢?

5.1 服务暴露组件

平时开发中 我们常用 接口 进行解耦,对接口的实现不用关心,避免接口调用与业务逻辑实现紧密关联。这里组件间的解耦也是相同的思路,仅依赖和调用服务接口,不会依赖接口的实现。

可能你会有疑问了:既然首页组件可以访问购物车组件接口了,那就需要依赖购物车组件啊,这俩组件还是耦合了啊,那咋办啊?答案是组件拆分出可暴露服务。见下图: 组件服务接口暴露 左侧是组件间可以调用对方服务 但是有依赖耦合。右侧,发现多了export_homeexport_cart,这是对应拆分出来的专门用于提供服务的暴露组件。操作说明如下:

  • 暴露组件 只存放 服务接口、服务接口相关的实体类、路由信息、便于服务调用的util等
  • 服务调用方 只依赖 服务提供方的 露组件,如module_home依赖export_cart,而不依赖module_cart
  • 组件 需要依赖 自己的暴露组件,并实现服务接口,如module_cart依赖export_cart 并实现其中的服务接口
  • 接口的实现注入 依然是由ARouter完成,和页面跳转一样使用路由信息

下面按照此方案 来实施 首页调用购物车服务 来获取商品数量,更好地说明和理解。

5.2 实施

5.2.1 新建export_cart

首先,在购物车工程中新建module即export_cart,在其中新建接口类ICartService并定义获取购物车商品数量方法,注意接口必须继承IProvider,是为了使用ARouter的实现注入:

/**
 * 购物车组件对外暴露的服务
 * 必须继承IProvider
 * @author hufeiyang
 */
public interface ICartService extends IProvider {

    /**
     * 获取购物车中商品数量
     * @return
     */
    CartInfo getProductCountInCart();
}
复制代码

CartInfo是购物车信息,包含商品数量:

/**
 * 购物车信息

 * @author hufeiyang
 */
public class CartInfo {

    /**
     * 商品数量
     */
    public int productCount;
}
复制代码

接着,创建路由表信息,存放购物车组件对外提供跳转的页面、服务的路由地址:

/**
 * 购物车组件路由表
 * 即 购物车组件中 所有可以从外部跳转的页面 的路由信息
 * @author hufeiyang
 */
public interface CartRouterTable {

    /**
     * 购物车页面
     */
    String PATH_PAGE_CART = "/cart/cartActivity";

    /**
     * 购物车服务
     */
    String PATH_SERVICE_CART = "/cart/service";

}
复制代码

前面说页面跳转时是直接使用 路径字符串 进行路由跳转,这里是和服务路由都放在这里统一管理。

然后,为了外部组件使用方便新建CartServiceUtil:

/**
 * 购物车组件服务工具类
 * 其他组件直接使用此类即可:页面跳转、获取服务。
 * @author hufeiyang
 */
public class CartServiceUtil {

    /**
     * 跳转到购物车页面
     * @param param1
     * @param param2
     */
    public static void navigateCartPage(String param1, String param2){
        ARouter.getInstance()
                .build(CartRouterTable.PATH_PAGE_CART)
                .withString("key1",param1)
                .withString("key2",param2)
                .navigation();
    }

    /**
     * 获取服务
     * @return
     */
    public static ICartService getService(){
        //return ARouter.getInstance().navigation(ICartService.class);//如果只有一个实现,这种方式也可以
        return (ICartService) ARouter.getInstance().build(CartRouterTable.PATH_SERVICE_CART).navigation();
    }

    /**
     * 获取购物车中商品数量
     * @return
     */
    public static CartInfo getCartProductCount(){
        return getService().getProductCountInCart();
    }
}
复制代码

注意到,这里使用静态方法 分别提供了页面跳转、服务获取、服务具体方法获取。 其中服务获取 和页面跳转 同样是使用路由,并且服务接口实现类 也是需要添加@Route注解指定路径的。

到这里,export_cart就已经准备完毕,我们同样发布一个export_cart的ARR(“com.github.hufeiyang.Cart:export_cart:xxx”)。

再来看看module_cart对服务接口的实现。

5.2.2 module_cart的实现

首先,module_cart需要依赖export_cart:

//module_cart的Build.gradle
dependencies {
    ...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'

    //依赖export_cart
    implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5'
}
复制代码

点击sync后,接着CartActivity的path改为路由表提供:

@Route(path = CartRouterTable.PATH_PAGE_CART)
public class CartActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cart);
    }
}
复制代码

然后,新建服务接口的实现类来实现ICartService,添加@Route注解指定CartRouterTable中定义的服务路由

/**
 * 购物车组件服务的实现
 * 需要@Route注解、指定CartRouterTable中定义的服务路由
 * @author hufeiyang
 */
@Route(path = CartRouterTable.PATH_SERVICE_CART)
public class CartServiceImpl implements ICartService {

    @Override
    public CartInfo getProductCountInCart() {
    	//这里实际项目中 应该是 请求接口 或查询数据库
        CartInfo cartInfo = new CartInfo();
        cartInfo.productCount = 666;
        return cartInfo;
    }

    @Override
    public void init(Context context) {
        //初始化工作,服务注入时会调用,可忽略
    }
}
复制代码

这里的实现是直接实例化了CartInfo,数量赋值666。然后发布一个ARR(“com.github.hufeiyang.Cart:module_cart:xxx”)。

5.2.3 module_home中的使用和调试

module_home需要依赖export_cart:

//module_home的Build.gradle
dependencies {
    ...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'

    //注意这里只依赖export_cart(module_cart由壳工程引入)
    implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5'
}
复制代码

在HomeActivity中新增TextView,调用CartServiceUtil获取并展示购物车商品数量:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        //跳转到购物车页面
        findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通过路由跳转到 购物车组件的购物车页面(但没有依赖购物车组件)
//                ARouter.getInstance()
//                        .build("/cart/cartActivity")
//                        .withString("key1","param1")//携带参数1
//                        .withString("key2","param2")//携带参数2
//                        .navigation();

                CartServiceUtil.navigateCartPage("param1", "param1");
            }
        });

        //调用购物车组件服务:获取购物车商品数量
        TextView tvCartProductCount = findViewById(R.id.tv_cart_product_count);
        tvCartProductCount.setText("购物车商品数量:"+ CartServiceUtil.getCartProductCount().productCount);
    }
}
复制代码

看到 使用CartServiceUtil.getCartProductCount()获取购物车信息并展示,跳转页面也改为了CartServiceUtil.navigateCartPage()方法。

到这里home组件的就可以独立调试了:页面跳转和服务调用,独立调试ok后 再集成到壳工程。 先让HomePage工程的app模块依赖Common组件、module_cart 以及本地的module_home

//HomePage工程,app模块的Build.gradle
dependencies {
    ...
    //引入本地Common组件、module_cart、module_home,在app module中独立调试使用
    implementation 'com.github.hufeiyang:Common:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6'

    implementation project(path: ':module_home')
}
复制代码

然后新建MyApplication初始化ARouter、在app的MainActivity中使用ARouter.getInstance().build("/homepage/homeActivity").navigation()打开首页,这样就可以调试了。

调试ok后接着就是集成到壳工程。

5.2.4 集成到壳工程

壳工程中的操作和独立调试类似,区别是对首页组件引入的是ARR:

dependencies {
    ...
    //这里没有使用私有maven仓,而是发到JitPack仓,一样的意思~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6'
    implementation 'com.github.hufeiyang:HomePage:1.0.4'

    //壳工程内 也需要依赖Common组件,因为需要初始化ARouter
    implementation 'com.github.hufeiyang:Common:1.0.0'
}
复制代码

最后run壳工程来看下效果: 获取数量666、跳转也ok 获取数量是666、跳转页面成功。

另外,除了export_xxx这种方式,还可以添加一个 ComponentBase 组件,这个组件被所有的Common组件依赖,在这个组件中分别添加定义了业务组件可以对外提供访问自身数据的抽象方法的 Service。相当于把各业务组件的export整合到ComponentBase中,这样就只添加了一个组件而已。但是这样就不好管理了,每个组件对外能力的变更都要改ComponentBase。

另外,除了组件间方法调用,使用EventBus在组件间传递信息也是ok的(注意Event实体类要定义在export_xxx中)。

好了,到这里组件间通信问题也解决了。

六、fragment实例获取

上面介绍了Activity 的跳转,我们也会经常使用 Fragment。例如常见的应用主页HomeActivity 中包含了多个属于不同组件的 Fragment、或者有一个Fragment多个组件都需要用到。通常我们直接访问具体 Fragment 类来new一个Fragment 实例,但这里组件间没有直接依赖,那咋办呢?答案依然是ARouter

先在module_cart中创建CartFragment:

//添加注解@Route,指定路径
@Route(path = CartRouterTable.PATH_FRAGMENT_CART)
public class CartFragment extends Fragment {
    ...
    public CartFragment() {
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        //显示“cart_fragment"
        return inflater.inflate(R.layout.fragment_cart, container, false);
    }
}
复制代码

同时是fragment添加注解@Route,指定路由路径,路由还是定义在export_cart的CartRouterTable中,所以export_cart需要先发一个ARR,module_cart来依赖,然后module_cart发布ARR。

然后再module_home中依赖export_cart,使用ARouter获取Fragment实例:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        ...
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction transaction= manager.beginTransaction();

		//使用ARouter获取Fragment实例 并添加
        Fragment userFragment = (Fragment) ARouter.getInstance().build(CartRouterTable.PATH_FRAGMENT_CART).navigation();
        transaction.add(R.id.fl_test_fragment, userFragment, "tag");
        transaction.commit();
    }
}
复制代码

可以先独立调试,然后集成到壳工程——依赖最新的module_cart 、HomePage,结果如下:

在这里插入图片描述

绿色部分就是引用自cart组件的fragment。

七、Application生命周期分发

我们通常会在Application的onCreate中做一些初始化任务,例如前面提到的ARouter初始化。而业务组件有时也需要获取应用的Application,也要在应用启动时进行一些初始化任务。

你可能会说,直接在壳工程Application的onCreate操作就可以啊。但是这样做会带来问题:因为我们希望壳工程和业务组件 代码隔离(虽然有依赖),并且 我们希望组件内部的任务要在业务组件内部完成。

那么如何做到 各业务组件 无侵入地获取 Application生命周期 呢?——答案是 使用AppLifeCycle插件,它专门用于在Android组件化开发中,Application生命周期主动分发到组件。具体使用如下:

  1. common组件依赖 applifecycle-api

首先,common组件通过 api 添加 applifecycle-api 依赖 并发布ARR:

//common组件 build.gradle
dependencies {
    ...
    //AppLifecycle
    api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
}
复制代码
  1. 业务组件依赖applifecycle-compiler、实现接口+注解

各业务组件都要 依赖最新common组件,并添加 applifecycle-compiler 的依赖:

//业务组件 build.gradle
...
	//这里Common:1.0.2内依赖了applifecycle-api
    implementation 'com.github.hufeiyang:Common:1.0.2'
    annotationProcessor 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'
复制代码

sync后,新建类来实现接口IApplicationLifecycleCallbacks用于接收Application生命周期,且添加@AppLifecycle注解。

例如 Cart组件的实现:

/**
 * 组件的AppLifecycle
 * 1、@AppLifecycle
 * 2、实现IApplicationLifecycleCallbacks
 * @author hufeiyang
 */
@AppLifecycle
public class CartApplication implements IApplicationLifecycleCallbacks {

    public  Context context;

    /**
      * 用于设置优先级,即多个组件onCreate方法调用的优先顺序
      * @return
     */
    @Override
    public int getPriority() {
        return NORM_PRIORITY;
    }

    @Override
    public void onCreate(Context context) {
        //可在此处做初始化任务,相当于Application的onCreate方法
        this.context = context;

        Log.i("CartApplication", "onCreate");
    }

    @Override
    public void onTerminate() {
    }

    @Override
    public void onLowMemory() {
    }

    @Override
    public void onTrimMemory(int level) {
    }
}
复制代码

实现的方法 有onCreate、onTerminate、onLowMemory、onTrimMemory。最重要的就是onCreate方法了,相当于Application的onCreate方法,可在此处做初始化任务。 并且还可以通过getPriority()方法设置回调 多个组件onCreate方法调用的优先顺序,无特殊要求设置NORM_PRIORITY即可。

  1. 壳工程引入AppLifecycle插件、触发回调

壳工程引入新的common组件、业务组件,以及 引入AppLifecycle插件:

//壳工程根目录的 build.gradle

buildscript {
   
    repositories {
        google()
        jcenter()

        //applifecycle插件仓也是jitpack
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'

        //加载插件applifecycle
        classpath 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-plugin:1.0.3'
    }
}
复制代码
//app module 的build.gradle

apply plugin: 'com.android.application'
//使用插件applifecycle
apply plugin: 'com.hm.plugin.lifecycle'
...
dependencies {
    ...
    //这里没有使用私有maven仓,而是发到JitPack仓,一样的意思~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.11'
    implementation 'com.github.hufeiyang:HomePage:1.0.5'

    //壳工程内 也需要依赖Common组件,因为要 触发生命周期分发
    implementation 'com.github.hufeiyang:Common:1.0.2'
}
复制代码

最后需要在Application中触发生命周期的分发:

//壳工程 MyApplication
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ...

        ApplicationLifecycleManager.init();
        ApplicationLifecycleManager.onCreate(this);
    }

    @Override
    public void onTerminate() {
        super.onTerminate();

        ApplicationLifecycleManager.onTerminate();
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();

        ApplicationLifecycleManager.onLowMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);

        ApplicationLifecycleManager.onTrimMemory(level);
    }
}
复制代码

首先在inCreate方法中调用 ApplicationLifecycleManager的init()方法,用于收集组件内实现了IApplicationLifecycleCallbacks且添加了@AppLifecycle注解的类。然后在各生命周期方法内调用对应的ApplicationLifecycleManager的方法,来分发到所有组件。

这样 组件 就能接收到Application的生命周期了。 新增组件的话,只需要 实现IApplicationLifecycleCallbacks并添加了@AppLifecycle注解 即可,无需修改壳工程,也不用关心

AppLifecycle插件是使用了 APT技术、gradle插件技术+ASM动态生成字节码,在编译阶段就已经完成了大部分工作,无性能问题、且使用方便。

到这里,组件化开发的5个问题点 都已经解决了。 下面来看看针对老项目如何实现组件化改造。

八、 老项目组件化

通常情况 我们去做组件化,都是为了改造 已有老项目。可能老项目内部的模块之间耦合严重,没有严格的业务模块划分,并且组件化改造是大工作量的事情,且要全量回归测试,总体来说,是需要全员参与、有较大难度的事情。

8.1 方案

8.1.1 组件划分

根据前面介绍的组件化架构图,组件分为 基础组件、业务基础组件、业务组件。

  • 基础组件,不用多说,就是基础功能,例如网络请求、日志框架、图片加载,这些与业务毫无关联,可用于公司所有项目,是底层最稳定的组件。这里就比较容易识别和拆分。
  • 业务基础组件,主要是供业务组件依赖使用,例如 分享、支付组件,通常是一个完整的功能,是较为最稳定的组件。这部分通常也是比较容易识别的。
  • 业务组件,完整的业务块,例如前面提到京东的 “首页”、“分类”、“发现”、“购物车”、“我的”。业务组件是日常需求开发的主战场。

8.1.2 组件拆分:基础组件、Common组件

基础组件最容易拆分,它依赖最少,功能单一纯粹。把基础组件依赖的东西,从老工程中抽取出来,放在单独的工程,做成单独的组件,发布ARR到公司maven仓。注意不能存在任何业务相关代码。

新建Common组件,使用 “api” 依赖 所有基础组件,这样依赖 Common组件的组件 就能使用所有基础组件的功能了。接着,就是前面提到的 ARouter、AppLifeCycle、以及其他第三方库的依赖。

另外,Common组件,还有一个重要部分:提供BaseActivity、BaseFragment,这里Base需要完成基础能力的添加,例如页面进入、退出的埋点上报、统一页面标题样式、打开关闭EventBus等等。

8.1.3 组件拆分:业务基础组件、业务组件

业务基础组件 基本上只依赖common,功能也是单一纯粹。同样是把依赖的东西抽取出来,放在单独的工程,做成单独的组件,发布ARR到公司maven仓。

业务组件,首先要识别组件的边界,可以按照页面入口和出口作为判断。然后,需要识别对 业务基础组件的依赖;以及 最重要的,对其他 业务组件的依赖。 可以先把代码抽离到单独的工程,然后依赖common组件、需要的业务基础组件,此时依然报错的地方就是 对其他 业务组件的依赖了。这时就可以给对应组件负责人提需求,在export_xxx中提供跳转和服务。 然后你只需要依赖export_xxx使用即可

老项目组件化改造需要循序渐进,除非有专门的时间。一般是需求开发和改造并行。要先完成一个组件,之后有了经验,后面其他业务组件陆续实施,这样就会比较简单。

8.2 常见问题

8.2.1 组件中butterknife报错—R2

在Library中,ButterKnife注解中使用R.id会报错,例如common组件 module_common 中新建Activity,并依赖butterknife:

android {
  ...
  // Butterknife requires Java 8.
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  implementation 'com.jakewharton:butterknife:10.2.3'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}
复制代码

报错如下所示: 在Library中,ButterKnife注解中使用R会报错 解决方法:需要添加ButterKnife插件,然后使用R2:

buildscript {
  repositories {
    mavenCentral()
    google()
  }
  dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
  }
}
复制代码
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
复制代码

然后ButterKnife注解中使用 R2 就ok了:

在Library中,ButterKnife注解中使用R2 到这里,Android组件化的知识全部讲完。

八、总结

本文介绍了 组件化开发的背景、架构、优势、要解决的问题 以及详细解决方案,独立调试、页面跳转、组件通信等,最后介绍的老项目组件化方案。

其中涉及的最重要的工具是ARouter,专门用于Android组件化解耦。ARouter还有很多进阶用法,有机会我也针对ARouter写一篇全面分析。还有一个重要知识点AppLifecycle插件,它的原理涉及APT、ASM插入字节码、gradle插件等技术,后续也会专门去分析这块知识。

Android开发组件化,是在项目发展到一定规模后 必定要使用的技术,学习至完全掌握非常必要。

好了,今天就到这里,欢迎留言讨论~

.

Demo的GitHub地址:

壳工程ComponentLearning

Common组件

HomePage组件

Cart组件

AppLifecycle插件

.

参考与感谢:

《Android 组件化最佳实践》

《Android组件化开发实践系列》

.

你的 点赞、评论,是对我的巨大鼓励!

欢迎关注我的 公 众 号

公众号:胡飞洋

🏆 掘金技术征文|双节特别篇