【Android】快速上手 Android 组件化开发

中型直播 APP 想要进行业务解耦分层的话,我们可以将其分为直播功能、登录功能和支付功能,这些功能本身在小型项目中可以通过分包解决,但当业务量上升时,因协作和编译难度增加,分包仍然会使得项目结构混乱复杂,让开发者捉襟见肘,所以我们要有更解耦的分离模式来划分业务层,于是就有本篇博客的主题------组件化开发,通过组件化开发可以实现业务模块间互不影响,各模块按需加载,编译速度提升的同时,项目不会越写越深。

关于组件化开发我们只要搞清楚三件事:模块之间如何相互知晓、模块之间如何相互跳转和如何独立编译模块,即依赖管理路由通信壳工程

模块划分

将模块按照职责分层,可以分为主层、业务模块层和基础库层,延续直播 APP 的思路可以分为 app、login、live、pay 和 common 模块。

Android Studio 有两种模块 application 和 library,前者可以编译成 apk 直接在设备安装运行,后者会编译成 arr 文件,不能单独运行,必须被 application 或其他 library 所使用。所以 Android 开发的业务模块和基础库可以作为 library 模块,app 作为宿主把业务模块和基础库集成编译运行。

因为 library 模块独立编译得到的 arr 文件不能单独运行,且在协作开发里,各开发者往往负责单业务模块的开发和调试,这让调试成为难题,所以才有壳工程的概念,也就是如何独立编译 library 模块。

依赖管理 指模块持有另一模块的依赖,这种持有关系和我们开发单模块项目时引入 material 库等各种官方或第三库的形式相同,都是使用模块 build.gradle 文件的 dependencies 块进行引入,这种依赖是静态依赖,主层、业务模块层和基础库层的依赖关系通常为:主层 依赖业务模块和基础库,将各模块集成运行;业务模块层只依赖基础库层,业务模块间尽量不互相依赖,若业务模块有逻辑重合,则尽量将重合逻辑移交至基础库层。

但这样又引出新的问题:业务模块间如何进行通信?这就是我们前面提到的路由通信概念,路由通信我们会例用 ARouter 实现。前面提到依赖是静态依赖,在编译期就会确定依赖关系,被依赖模块的变动会导致依赖模块重新编译,所以路由通信的目的是为解耦模块,解决在避免各业务模块相互依赖的情况无法进行通信的问题,模块间可以只依赖路由接口或统一协议,实现动态依赖。

回到模块划分,我们在 Android Studio 设计项目的模块结构如图,可以看到除 app 模块外,业务模块和基础库都是 library,这样就完成基本的模块划分操作。

依赖管理

依赖管理的步骤我们前面有提到,具体方法是找到各模块 build.gradle 的 dependencies 依赖块,添加 implementation project(":common") 形式的语句来引依赖。

值得注意的是,项目模块间的依赖关系要避免循环依赖,直观原因是 Gradle 编译时会报错,其次若有循环依赖则每次修改都可能触发完整依赖链的重新编译,最后是因为可能引发运行时问题:如 A 模块调用 B 模块的静态方法,但 B 模块的静态块又引用 A 模块的类,该类可能在初始化还没完成时就被访问。

在结构复杂业务繁多的超大型项目中可以使用动态化依赖方案,在运行时动态调用其他模块,实现方法是 Dynamic Feature Module,这种方式编写代码的难度较高,我们在中型项目只要使用 ARouter 即可。

路由通信

依赖可以将模块 A 引用模块 B 的类信息,若 A 和 B 之间没有依赖关系,则不能使用显式 Intent 进行跳转,隐式 Intent 能够跳转但不推荐这样做。比较主流的方法是使用路由通信技术,路由通信的原理是在编译期收集路径到目标类的映射表,运行时按路径查表以进行跳转或服务调用,映射表的实现要借助 APT 注解生成器。我们以 ARouter 开源工具为例看如何使用路由通信实现独立模块间通信。

ARouter API 文档

ARouter 分有 4 个模块,分别是 ARouter 的核心 API、注解生成器、ASM 插桩工具(作用是加速 ARouter 加载路由表)和自动生成模板的开发插件。有通信需求的模块分有两种情况:跳转到其他模块的 UI 界面跨模块提供服务,前者情况的发起跳转方要有 api 模块依赖,目标方可以只暴露路径来让注解生成器生成路由表,所以只要有 compiler 的依赖即可;后者情况的双方都要依赖 api 模块,因为此时的服务提供方不仅要注册路由表,还要在运行时加载实例,服务调用方要通过 ARouter 获取提供方实例。

首先在模块对应 build.gradle 文件的 android - defaultConfig 块加入代码,因为注解生成器生成路由表会按模块分组,所以要传入各模块名避免混淆和覆盖,javaCompileOptions 表示 Java 代码运行传入的参数。

groovy 复制代码
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [AROUTER_MODULE_NAME: project.getName()]
    }
}

接着在 dependencies 块加入 implementation "com.alibaba:arouter-api:1.5.2"annotationProcessor "com.alibaba:arouter-compiler:1.5.2",这样我们就完成 ARouter 的依赖配置操作。

在支持路由的类加入注解和路径参数,路径参数至少为两级,如果路由类非 Activity 或 Fragment,也就是类想要提供服务,则要实现 IProvider 接口,此时通过路由返回的是提供功能的服务实例。

java 复制代码
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}

然后要进行调试和初始化,init 方法有 Context 参数和无参两种重载形式,无参初始化只完成基本的框架初始化,内部没有 Context 实例,因为 IProvider 要通过 Context 进行初始化,所以无参初始化的 ARouter 不具备提供服务的功能,且后续的 navigation 功能如果对 Activity 使用相当于调用 startActivity 方法,同样也要有 Context 的参与。

java 复制代码
if (isDebug()) {
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

发起路由操作的 navigation 方法同样有 Context 参数和无参形式的重载,无参形式对 IProvider 会返回单例服务实例,对 Fragment 会返回新建的 Fragment 实例,对 Activity 会直接调用 startActivity 方法;Context 参数调用则保证 Activity 跳转场景和首次获得 IProvider 场景不发生异常。

java 复制代码
ARouter.getInstance().build("/test/activity").navigation();

ARouter.getInstance().build("/test/activity")
            .with(new Bundle()) // 携带参数
            .navigation();

壳工程

配置壳工程前我们要了解常见 gradle 文件的作用,还是以我们在开始提到的 LiveDemo 分包为例。

  1. 项目 build.gradle:控制项目的全局插件、仓库地址和脚本;
  2. settings.gradle:首要作用是声明项目名和模块名,规定 gradle 查找和下载插件的路径;
  3. 模块 build.gradle:最常用的 gradle 文件,内部的 plugin 块表明该模块是 application 还是 library,dependencies 块声明该模块加载的各项依赖;
  4. gradle.properties:加载项目所有 gradle 文件都可以访问的全局变量。

为实现 library 模块独立编译运行调试,我们要做 application 模拟,同时要保证 debug 和 release 要轻松切换,实现方法是在 gradle.properties 添加字段 isDebug = false,true 表示独立测试,false 表示集成测试,接着对业务模块 build.gradle 的 plugin 块进行修改。值得注意的是 gradle.properties 声明的全局变量是 String 类型,在 build.gradle 要转换成布氏值。

groovy 复制代码
/* gradle.properties */
isDebug = false

/* build.gradle */
plugins {
alias(libs.plugins.android.library)
}

/* 改为 */
def isDebugApp = isDebug.toBoolean()
if (isDebugApp) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

此外注意 library 可能在 defaultConfig 块声明专有文件 consumer-rules.pro,要加条件判断。

groovy 复制代码
if (!isDebugApp) {
    consumerProguardFiles "consumer-rules.pro"
}

接着我们要设置测试用的启动页和 ApplicationId,同样在模块 build.gradle 的 defaultConfig 块加入条件判断。

groovy 复制代码
if (isDebugApp) {
    applicationId "com.example.live.debug"
}

然后在模块 src 目录创建 debug Directory,创建后在内部创建 java Directory 和 AndroidManifest.xml。

编辑 src/debug 新创建的 AndroidManifest 设置独立测试的启动页,这里我们用的是 MainActivity,值得注意的是我们在 src/debug 创建的 java 文件夹用来存放仅在独立测试时编译和运行生效的 java 代码,可以包含日志或数据拟真等功能,集成测试时不会包含这些类。

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:allowBackup="true"
        android:label="LiveDemo"
        android:theme="@style/Theme.AppCompat">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

完成操作后再回到模块的 build.gradle,在 android 块加入 sourceSets 来区分加载不同的 AndroidManifest 文件。

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

我们注意到最开始设置的 isDebug 字段只有初始值,而没有决定何时何值的逻辑,这也是本次介绍壳工程实现方式的明显缺点,只能通过在 gradle.properties 手动修改 isDebug 值来实现独立编译和集成编译的切换,因为单独编译本身是临时环境,所以这种方式是可以接受的。

尾声

文章介绍了各模块的依赖原则、ARouter 的简单使用以及单工程项目如何进行壳工程化等内容,这些足够应对业务模块少量项目的情况,如果读者希望更加理解组件化原理,可以学习 Android 包结构、Gradle 构建系统和 ARouter 源码等内容。

相关推荐
那我掉的头发算什么1 小时前
【javaEE】多线程进阶--CAS与原子类
android·java·jvm·java-ee·intellij-idea
Yue丶越1 小时前
【Python】基础语法入门(二)
android·开发语言·python
q***08741 小时前
MySQL压缩版安装详细图解
android·mysql·adb
九鼎创展科技1 小时前
九鼎创展发布X3588SCV4核心板,集成LPDDR5内存,提升RK3588S平台性能边界
android·人工智能·嵌入式硬件·硬件工程
拾忆,想起2 小时前
Dubbo网络延迟全链路排查指南:从微服务“快递”到光速传输
网络·网络协议·微服务·架构·php·dubbo
励志成为糕手2 小时前
Flume架构深度解析:构建高可用大数据采集系统
大数据·架构·flume·日志·大数据采集
与籍同行2 小时前
安卓10.0 分屏相关
android
BD_Marathon3 小时前
Eclipse 代码自动补全设置
android·java·eclipse
settingsun12253 小时前
分布式系统架构:SQL&NoSQL
sql·架构·nosql