一、什么是组件化
随APP的不断更新,代码也逐渐复杂,开发效率下降,单一工程下代码耦合严重,当需要调试的时候非常耗时,单独修改一个模块无法单独测试。
二、使用组件化的目的
组件化框架的目的是让各个项目变得独立,各个组件在组件模式下可以独立开发,在集成模式下有可以重新集成到“app壳工程”中,从而得到一个完整的app
1.方便调试
2.团队开发
3.提升编译速度
4.超级解耦:方便后期维护
5.功能重用:某一个模块需要使用另一个模块的功能只需要依赖这个模块
三、组件化的理解
3.1 模块化
在介绍组件化之前,先说说模块化。我们知道在Android Studio中,新建工程默认有一个App module,然后还可以通过File->New->New Module 新建module。那么这里的“module” 实际和我们说的“模块”基本是一个概念了。也就是说,原本一个 App模块 承载了所有的功能,而模块化就是拆分成多个模块放在不同的Module里面,每个功能的代码都在自己所属的 module 中添加。
已京东为例,大致可以分为 “首页”、“分类”、“发现”、“购物车”、“我的”、“商品详情” 六个模块。
项目结构
这是一般项目都会采用的结构。另外通常还会有一个通用基础模块module_common,提供BaseActivity/BaseFragment、图片加载、网络请求等基础能力,然后每个业务模块都会依赖这个基础模块。 那么业务模块之间有没有依赖呢?很显然是有的。例如 “首页”、“分类”、“发现”、“购物车”、“我的”,都是需要跳转到“商品详情” 的,必然是依赖“商品详情” ;而“商品详情”是需要能添加到“购物车”能力的;而“首页”点击搜索显然是“分类”中的搜索功能。所以这些模块之间存在复杂的依赖关系。
模块化 在各个业务功能比较独立的情况下是比较合理的,但多个模块中肯定会有页面跳转、数据传递、方法调用 等情况,所以必然存在以上这种依赖关系,即模块间有着高耦合度。 高耦合度 加上 代码量大,就极易出现上面提到的那些问题了,严重影响了团队的开发效率及质量。
为了 解决模块间的高耦合度问题,就要进行组件化了。
3.2 组件化介绍
组件化去除模块的耦合,使得每个模块可以独立当APP存在,对于其他模块没有直接的依赖关系。此时业务模块就成为了业务组件。
而除了业务组件还有抽离出来的业务基础组件,提供业务组件使用,但不是独立的业务,例如:分享组件,广告组件;还有基础组件,即单独的基础功能,与业务无关,例如 图片加载、网络请求等。这些后面会详细说明。
组件化带来的好处
1.加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
2.提高团队开发:解耦 使得组件之间 彼此互不打扰,组件内部代码相关性极高。 团队中每个人有自己的责任组件,不会影响其他组件;降低团队成员熟悉项目的成本,只需熟悉责任组件即可;对测试来说,只需重点测试改动的组件,而不是全盘回归测试。
3.功能重用:组件 类似我们引用的第三方库,只需维护好每个组件,一建引用集成即可。业务组件可上可下,灵活多变;而基础组件,为新业务随时集成提供了基础,减少重复开发和维护工作量。
下图是我们期待的组件化框架
1.组件依赖关系是上层依赖下次,上层的修改频率高于下层
2.基础组件是通用基础能力,修改频率极低
3.common组件,作为支撑业务组件,业务基础组件的基础,同时依赖所有基础组件,提供业务组件需要的基础功能,统一了基础组件的版本号。所以业务组件和基础业务组件所需的基本功能只需要依赖common组件即可获得
4.业务组件,业务基础组件,都依赖于common组件。但业务组件之间不存在依赖关系,业务基础组件不存在依赖关系。而业务组件是依赖所需的所有业务基础组件,例如几乎所有业务组件都会依赖组件广告组件来展示Banner广告,弹窗广告。
5.最上层是主工程,即所谓的“壳工程”,主要集成所有的业务组件,提供Application唯一实现,gradle,manifes配置,整合成完备的APP
3.3组件化开发的问题点
核心问题是业务组件去耦合。那么存在哪些耦合的情况呢?前面有提到过,页面跳转、方法调用、事件通知。 而基础组件、业务基础组件,不存在耦合的问题,所以只需要抽离封装成库即可。 所以针对业务组件有以下问题:
1.业务组件如何单独运行调试
2.业务组件没有依赖如何进行跳转
3.业务组件间 没有依赖如何实现组件间通信、方法调用
4.业务组件间 没有依赖如何获取fragment
5.业务组件不能反向依赖壳工程,如何获取Application实例,如何获取Application onCreate()回调(用于初始化)。
四、组件独立调试
每个业务组件都是一个完整的整体,可以当作独立的APP,需要满足独立运行以及调试的要求,这样可以提高编译的速度。
做到独立调试有两种方案:
1.单工程方案,组件以module形式存在,动态配置组件的工程类型
2.多工程方案,组件以library module形式存在于独立的工程,且只有这一个library module
4.1 单工程方案
4.1.1 动态配置组件的工程类型
单工程模式,整个项目是一个工程,它包含:App module加上各个业务moudule,就是所有的代码,这就是单工程。
如何做到单独调试?
我们知道,在 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 插件。想要实现 业务组件的独立调试,这就需要把配置改为 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'
}
4.1.2动态配置ApplicationId和AndroidManifest
我们知道一个App需要一个ApplicationId的,而组件在单独调试时也是一APP,所以也需要一个ApplicationId,集成调试时组件是不需要ApplicationId;另外一个APP也只有一个启动页,而组件在独立调试时也需要一个启动页,在集成调试时就不需要了。所以ApplicationId和Manifest也是需要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文件路径。
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”构建结果如下:
4.2 多工程方案
4.2.1方案概述
多工程方案,业务组件以library module形式存在于独立的工程。独立工程 自然就可以独立调试了,不再需要进行上面那些配置了。
例如,购物车组件 就是 新建的工程Cart 的 module_cart模块,业务代码就写在module_cart中即可。app模块是依赖module_cart。app模块只是一个组件的入口,或者是一些demo测试代码。
当所有的业务组件都是拆分为独立的组件时,原本的工程就变成了一个只有app模块的壳工程,壳工程就是用来基础所有业务组件的
4.2.1 maven引用组件
那么如何进行集成调试呢?
使用maven引用组件:
1.发布组件的arr包到公司的maven仓库
2.然后在壳工程中使用implement依赖就可以了,和使用,和使用第三方库一毛一样。另外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,需要先在壳工程的根目录下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引用来集成即可。并且业务组件和业务基础组件也可以和基础组件一样,可以给公司其他项目复用。
五、页面跳转
5.1方案一、ARouter
前面说到,组件化的核心就是解耦,所以组件间是不能有依赖的,那么如何实现组件间的页面跳转呢?
例如 在首页模块 点击 购物车按钮 需要跳转到 购物车模块的购物车页面,两个模块之间没有依赖,也就说不能直接使用 显示启动 来打开购物车Activity,那么隐式启动呢? 隐式启动是可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。这里我们采用业界通用的方式—路由。
比较著名的路由框架 有阿里的ARouter、美团的WMRouter,它们原理基本是一致的。
这里我们采用使用更广泛的ARouter:“一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦”。
5.2 ARoute实现路由跳转
前面提到,所有的业务组件都依赖了 Common 组件,所以我们在 Common 组件中使用关键字**“api”**添加的依赖,业务组件都能访问。 我们要使用 ARouter 进行界面跳转,需要Common 组件添加 Arouter 的依赖(另外,其它组件共同依赖的库也要都放到 Common 中统一依赖)。
5.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'
}
5.2.2 初始化
依赖完要先对ARouter初始化,需要在Appliaction内完成
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);
}
}
5.2.2 路由跳转
好了,准备工作都完成了。并且知道 首页组件是没有依赖购物车组件的,下面就来实现前面提到的 首页组件 无依赖 跳转到 购物车组件页面。
而使用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_home、export_cart,这是对应拆分出来的专门用于提供服务的暴露组件。操作说明如下:
暴露组件 只存放 服务接口、服务接口相关的实体类、路由信息、便于服务调用的util等
服务调用方 只依赖 服务提供方的 露组件,如module_home依赖export_cart,而不依赖module_cart
组件 需要依赖 自己的暴露组件,并实现服务接口,如module_cart依赖export_cart 并实现其中的服务接口
接口的实现注入 依然是由ARouter完成,和页面跳转一样使用路由信息
下面按照此方案 来实施 首页调用购物车服务 来获取商品数量,更好地说明和理解。
6.2实施
6.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对服务接口的实现。
6.2.2 moudule_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”)。
6.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后接着就是集成到壳工程。
6.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、跳转页面成功。
另外,除了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。
八、总结
本文介绍了 组件化开发的背景、架构、优势、要解决的问题 以及详细解决方案,独立调试、页面跳转、组件通信等,最后介绍的老项目组件化方案。
其中涉及的最重要的工具是ARouter,专门用于Android组件化解耦。