Android 组件化基础(一)—— 概述与基本配置

一、概述

1.1 组件化的意义

组件化是大型 app 的标配,它可以帮助解决项目开发过程中可能会遇到的一些问题,如:

  • 代码耦合:项目增大后易失去层次感,容易出现不同业务间的代码互相调用,高度耦合。组件化则可以实现各模块间不相互依赖,但可以互相交互、任意组合,高度解耦。
  • 编译时间长:项目代码越多编译时间越长。而组件化可以分模块打包进行编译测试。
  • 代码复用率低:不同业务间可能会出现重复的基础代码,但是并没有被抽离出来进行复用。组件化可以将基础组件或功能抽离出来进行复用(到新项目)。
  • 团队开发效率低:多人协作开发时,可能会由于代码风格不同而互相影响,也可能会增加代码版本管理成本或沟通成本。组件化将功能按照模块划分后可以一定程度上减轻以上问题从而提高效率。

1.2 组件化的成员

项目使用组件化后的大致构成:

使用组件化后,app 模块除了一些全局配置以及应用入口外,就不再包含具体的业务代码,也被称作 app 壳,它依赖于各个业务模块。

而各个业务模块(home、product、order、personal)之间平起平坐,不会相互依赖,但是可以通过路由进行通信,同时也可以单独打包出一个 apk 进行单独测试。

公共基础库会提供一些通用功能供上层,比如 Base 可以封装 BaseActivity/BaseFragment,Utils 工具类,Retrofit + OKHttp + RxJava 的网络访问组件以及提供组件化路由的 ARouter 等等。这部分在实际项目中可能会再细分出多个层次,需要根据项目灵活变化,由于我们聚焦的是组件化模块和路由,所以这部分就直接粗略的用一层表示了。

1.3 环境切换

组件化项目通常有两种环境,一种是组件化环境,一种是集成化环境:

  • 组件化环境:子模块能独立运行,可以独立打包出 apk。
  • 集成化环境:子模块不能独立运行,需要整个项目打包成一个 apk。

两种环境的切换通常是通过配置 Gradle 实现的,组件化模式时,app 以及子模块都是 Phone Module,子模块才能单独打包出 apk;而集成化模式时,只有 app 是 Phone Module,子模块需要配置成 Android Library:


观察 Phone Module 与 Android Library 的 build.gradle 文件,找出二者的区别,也就找到了在二者间切换的方法:

  1. Phone Module 应用的插件是 com.android.application,而 Android Library 则是 com.android.library。
  2. Phone Module 会在 android -> defaultConfig 节点下声明 applicationId,而 Android Library 则没有。

所以在配置子模块的 gradle 时,可以使用类似如下的方式实现环境切换:

// isRelease 表示是否为集成化模式
if (isRelease) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

android {
	defaultConfig {
        if (!isRelease) {
            applicationId com.xxx.xxxx
        }
}

完整的配置方式会在下一节介绍。

二、基本配置

2.1 创建模块

创建子模块 order 和 personal,选择 Phone & Tablet Module 好一点,因为 AS 会创建默认的 Activity 以及 res 目录,选择 Android Library 则不会(总结:选 Phone & Tablet Module 省事儿)。项目结构图中的【公共基础库】部分,我们就新建一个 Android Library 的 common 模块来模拟了:

2.2 配置模块的 build.gradle

然后配置各个模块下的 build.gradle 以及整个项目的 build.gradle,我们的想法是将子模块中相同的内容向上抽取到项目的 build.gradle 中(便于统一管理),再通过配置文件中的变量控制集成化与组件化的切换。在项目根目录下新建 config.gradle 文件,内容如下:

ext {
    // 表示是否为正式版本,即集成化环境
    isRelease = true

    // 建立 Map,以键值对形式存储版本信息变量
    androidId = [
            compileSdkVersion: 32,
            buildToolsVersion: "29.0.3",
            minSdkVersion    : 23,
            targetSdkVersion : 32,
            versionCode      : 1,
            versionName      : "1.0"
    ]

    // 所有模块的 Id
    appId = [
            app     : "com.demo.arouter",
            order   : "com.demo.order",
            personal: "com.demo.personal"
    ]

    // 所有模块都使用的依赖才存在这里
    dependencies = [
            "appcompat" : "androidx.appcompat:appcompat:1.3.1",
            "constraint": "androidx.constraintlayout:constraintlayout:2.0.4"
    ]
}

isRelease 属性其实就是切换组件化与集成化环境的开关。此外 config.gradle 文件需要在项目的 build.gradle 中引入才能生效:

apply from: 'config.gradle'

接下来才开始配置模块的 build.gradle,由于 app 模块与其它子模块还是有不同之处的:

  1. app 一直是一个 Phone & Tablet Module,即便是集成化环境时它也不会切换成 Android Library;
  2. app 在集成化环境时需要依赖其它业务子模块(在当前例子中就是要依赖 order 和 personal 模块)。

所以先看一下 app 的基本配置:

apply plugin: 'com.android.application'

def androidId = rootProject.ext.androidId
def appId = rootProject.ext.appId
def support = rootProject.ext.dependencies

android {
    compileSdkVersion androidId.compileSdkVersion
    buildToolsVersion androidId.buildToolsVersion

    defaultConfig {
        applicationId appId.app
        minSdkVersion androidId.minSdkVersion
        targetSdkVersion androidId.targetSdkVersion
        versionCode androidId.versionCode
        versionName androidId.versionName
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])

    // 应用所有在 config.gradle 中定义的依赖
    support.each { k, v -> implementation v }

    // 集成化模式,要发布版本时,子模块就都不能独立运行了
    if (isRelease) {
        implementation project(":module-order")
        implementation project(":module-personal")
    }

    implementation project(":lib-common")
}

注意一下应用依赖所使用的方式。前面我们在 config.gradle 中将所有模块共同需要依赖的库以键值对的形式保存在 Map dependencies 中,并且在 app 模块的 build.gradle 文件中取出 dependencies 并赋值给变量 support,each 实际上是 groovy 中提供的一个方法,作用相当于 Java 中的 foreach,即遍历集合中的所有元素。因此 support.each { k, v -> implementation v } 就是遍历 support 中的元素,并对每个元素执行 implementation value。

config.gradle 中定义的属性也可以以键值对的形式直接定义在 gradle.properties 文件中。

其它子模块的配置与 app 略有不同:

// 切换集成化环境与组件化环境
if (isRelease) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

def androidId = rootProject.ext.androidId
def appId = rootProject.ext.appId
def support = rootProject.ext.dependencies

android {
    compileSdkVersion androidId.compileSdkVersion
    buildToolsVersion androidId.buildToolsVersion

    defaultConfig {
        // 只有组件化环境时才需要执行 applicationId
        if (!isRelease) {
            applicationId appId.order
        }
        minSdkVersion androidId.minSdkVersion
        targetSdkVersion androidId.targetSdkVersion
        versionCode androidId.versionCode
        versionName androidId.versionName
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    sourceSets {
        main {
            if (!isRelease) {
                // 测试版本,组件化环境,让 /main/debug/ 下的 AndroidManifest 生效
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                // 正式版本,集成化环境,让 main 下的 AndroidManifest 生效
                manifest.srcFile 'src/main/AndroidManifest.xml'

                java {
                    // release 时 debug 目录下的文件不需要合并到主工程中,减小 apk 体积
                    exclude '**/debug/**'
                }
            }
        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    support.each { k, v -> implementation v }
    implementation project(":lib-common")
}

当处于集成化环境时,整个应用打包成一个 apk,程序入口在 app 模块中,但当切换成组件化模式时,子模块单独打包为一个 apk,需要指定一个入口 Activity,并且配置 Application 标签下的相关内容,因此采用通过 sourceSets 指定源集的方式指定不同环境下所使用的 AndroidManifest 文件。


到这里,组件化项目的基本配置就完成了。

三、组件间通信

组件化后,不同组件之间没有相互依赖,模块间的跳转就不能再通过 startActivity() 这种方式,比较常用的是阿里的 ARouter 路由框架,GitHub 的项目主页上对于使用方法介绍的已经很详细,这里我们就简单说说。

3.1 添加 ARouter 依赖与配置

官方给出的依赖与配置是这样的:

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    // Replace with the latest version
    compile 'com.alibaba:arouter-api:?'
    annotationProcessor 'com.alibaba:arouter-compiler:?'
    ...
}

适用到我们当前的例子中,就是公共基础库的 common 模块的 build.gradle,以如下方式添加 arouter-api 的依赖:

dependencies {
	...
    api 'com.alibaba:arouter-api:1.5.2'
}

而 app 及其他子模块添加注解处理器和模块名参数:

android {
	...
	defaultConfig {
		...
		javaCompileOptions {
            annotationProcessorOptions {
            	// 当前模块名作为参数
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
	}
}

dependencies {
	...
	// 注解处理器的依赖
	annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
}

因为 ARouter 是利用 APT 技术通过自动生成代码建立路由表从而支持模块间跳转的,因此模块中如果有 Activity/Fragment 需要加入路由表时,就需要为其加上注解,并由注解处理器扫描后才能被添加到路由表中,而建立路由表时需要用模块名作为参数,所以才需要用 javaCompileOptions -> annotationProcessorOptions 这种形式将模块名传递给注解处理器。

3.2 添加注解

给需要加入路由表的 Activity 添加 @Route 注解:

@Route(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {

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

@Route 注解的 path 值最好按照【/模块名/Activity名】的方式写,打上这个注解的 Activity 会被添加到路由表中,进而可以被跳转。

3.3 初始化 SDK

public class BaseApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            ARouter.openLog();
            ARouter.openDebug();
        }
        ARouter.init(this);
    }
}

初始化完成后,编译项目,会发现 ARouter 在各个模块下为我们生成了一些文件,正是这些文件帮我们完成了路由表的建立等工作:


当前先不用理解这些文件的生成规则与具体内容,后面文章会详细说明。

3.4 使用 ARouter 进行跳转

最基本的跳转功能:

	public void jumpToOrder(View view) {
        ARouter.getInstance().build("/order/Order_MainActivity").navigation();
    }

build() 中传的是被跳转的页面的路由地址:

@Route(path = "/order/Order_MainActivity")
public class Order_MainActivity extends AppCompatActivity {
	...
}

也可以带参数跳转,假如从 Order 跳转到 Personal:

@Route(path = "/order/Order_MainActivity")
public class Order_MainActivity extends AppCompatActivity {

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

    public void jumpToPersonal(View view) {
        ARouter.getInstance().build("/personal/Personal_MainActivity")
                .withString("key1", "TestString") 
                .withInt("key2", 66)
                .navigation();
    }
}

接收方的对应变量要打上 @AutoWired 注解,默认情况下,变量名就是参数传递的 key,或者也可以通过 name 指定:

@Route(path = "/personal/Personal_MainActivity")
public class Personal_MainActivity extends AppCompatActivity {

    @Autowired
    String key1;

    @Autowired(name = "key2")
    int count;

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

		// 如果想通过依赖注入自动接收参数值,要调用这个方法。
        ARouter.getInstance().inject(this);
        Toast.makeText(this, "key1 = " + key1 + ",count = " + count, Toast.LENGTH_LONG).show();
    }
}

两次跳转的效果图如下:

3.5 使用 ARouter 调用其它模块服务

假如,现在有一个需求,要在 personal 模块的页面上显示待发货、待收货、待评价的订单数量,理论上来说这些订单信息需要由 order 模块对外提供服务,再由 personal 模块调用这些服务以获取订单信息。由于子模块之间不存在依赖,因此不能直接调用,需要借助 ARouter 路由来实现。

首先,在 common 模块中定义接口 IOrderService,继承自 IProvider:

public interface IOrderService extends IProvider {

    int getBacklogCount();

    int getSendingCount();

    int getEvaluatingCount();
}

然后在 order 模块中实现该接口,并把实现类通过 @Route 注解添加进路由中:

@Route(path = "/order/OrderServiceImpl")
public class OrderServiceImpl implements IOrderService {

    @Override
    public int getBacklogCount() {
        return 4;
    }

    @Override
    public int getSendingCount() {
        return 3;
    }

    @Override
    public int getEvaluatingCount() {
        return 5;
    }

    @Override
    public void init(Context context) {

    }
}

最后在 personal 模块中通过依赖注入获取到 order 模块提供的 IOrderService 服务,调用相关方法:

@Route(path = "/personal/Personal_MainActivity")
public class Personal_MainActivity extends AppCompatActivity {

    @Autowired
    IOrderService orderService;

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

        ARouter.getInstance().inject(this);

        int backlogCount = orderService.getBacklogCount();
        int sendingCount = orderService.getSendingCount();
        int evaluatingCount = orderService.getEvaluatingCount();
        Toast.makeText(this, "待发货:" + backlogCount + ",待收货:" + sendingCount +
                ",待评价:" + evaluatingCount, Toast.LENGTH_LONG).show();
    }
}

效果图:

到这里,一个特别基础的组件化项目就算搭建完成了,下篇文章会模仿 ARouter 实现一个简单的路由框架:Android 组件化基础(二)—— 仿 ARouter 实现一个路由框架

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值