第一章 Android组件化初识

1.1 组件化和模块化的区别

  • 组件化的重心主要是放在业务逻辑层,主要是为了拆分业务逻辑,只针对业务逻辑
  • 模块化的重心主要是为了功能的重用,那功能拆分一个个的插件,针对整个项目

1.2 组件化和插件化的区别

  • 组件化我们不管把我们的业务拆分为多少个模块,最终在打包上线的时候我们都会生成一个apk
  • 而插件化也是拆分为很多插件模块,但是在最终打包之后,成为了很多apk,最终我们把它上传到我们的服务器上面,用户使用的时候,只需要下载响应的apk即可,然后使用动态加载技术,加载里面相应的Activity

1.3 组件化开发的优势

  • 相当于我们每次运行不需要整个项目运行,而是运行单一的组件即可
  • 如果我们要将某一个模块用到新项目中去,就很简单了,因为我们每一个模块都是一个独立的Application
  • 因为Application是不能依赖其他的Application的
  • 我们就不需要解耦合了、资源等等
  • 大团队开发中,组件化开发是开发的基石

1.4 业务逻辑层

  • 公共层
  • 系统层

1.5 组件化开发要遇到的问题

1.6 从组件化实战来解决问题

  • 创建组件化项目
  • 创建两个Application组件,注意修改MainActivity的类名,以防止资源命名冲突问题

  • 我们增加了这么多模块,那么每个模块里面都有build.gradle,假设后面我们需要升级某一个模块中依赖的组件的版本,我们需要修改每一个模块中的版本号么?这样肯定是不行的,所以我们需要创建一个文件,统一管理我们的版本号,而在各个模块中使用相同的版本控制工具才行。这里有三种方式:
    • 第一种方式:直接定义到gradle.properties,简单粗暴
    • 第二种方式:定义到我们整个项目的build中
    • 第三种方式:我们自己创建一个Gradle文件,专门去装我们这些参数
    • 我们采用第三种方式

  • 文件创建好了,但是我们的项目是不知道的,怎么让整个项目都能使用呢?
  • 在项目的build.gradle中声明即可

  • 这样声明了就是告诉项目在我们程序加载的时候去加载我们的config
  • 然后我们就可以在主App和子Module下面使用我们的config文件了

  • 改完之后,Sync now一下即可,没有报错就是成功了
  • 新的问题来了,我们通常修改完SDK版本号之后,我们下面的各种依赖库的版本也要与之对应,怎么办呢?
  • 当然,我们也可以通过同样的方式,定义一个数组来解决问题

  • 同样的方式,在各个组件的依赖下面引入即可

  • 写到这里,会有疑问,gradle文件里面,为什么有的直接用大括号?有的用等于号,有的用冒号,这里解释下,直接用大括号表示数据集的意思,后面等于号表示一个对象的意思,冒号则是某个属性啦
  • 写到这里,我们就将各种依赖库的版本号给统一管理啦
  • 下一个问题

1.6.1 组件如何在Application和各种Module之间来回切换?

  • 其实就是修改我们的组件的打包方式即可

  • 把此处的Application改为library
  • 但是如果我们每次都需要手动修改,就比较麻烦了
  • 我们可以添加一个开关

  • 然后在module中根据开关配置一下即可
if (rootProject.ext.android.is_application) {
  apply plugin: 'com.android.application'
} else {
  apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'

这个时候,我们把开关设置为false,就会发现,组件都变为Library了

1.6.2 AndroidManifest.xml文件的区分

  • 因为application和library的AndroidManifest.xml文件是不同的,这个时候我们就需要区分了
  • 我们再module模块中,添加一个manifest文件夹,并拷贝一份manifest文件进去
  • 然后library的manifest文件是不需要intent的,这个时候就需要我们去掉intent以及Application里面的部分代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.login">
  <application>
    <activity android:name=".LoginActivity">
  </activity>
</application>
</manifest>
  • 我们把每个Module里面的manifest修改了之后,就需要设置一下了,根据is_applitaion属性来动态设置manifest文件
  • 在每个module模块下面添加这样一段代码即可
sourceSets{
  main{
    if (rootProject.ext.android.is_application) {
      manifest.srcFile 'src/main/AndroidManifest.xml'
    } else {
      manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
    }
  }
}

1.6.3 library模块中不能有applicationId

  • 这个异常是因为什么呢?
    • 是因为我们的defaultConfig中设置了applicationId,但是library中是不需要的
  • 怎么解决这个异常呢?
    • 我们在设置applicationId的时候添加判断即可

1.6.4 模块设置

模块设置完毕了,那么最终我们需要发布的时候,就需要让我们的主App去依赖我们的Module了

  • 如上图所示,我们让App依赖下面的两个模块

1.6.5 依赖之后,我们已经发布过了,但是现在我们又需要将我们的模块修改为application怎么办呢?

  • 我们改为true,发现抛出了异常
  • 异常的意思就是我们的App模块不能依赖我么的login模块和member模块,因为都是application,application
  • 不能依赖application模块
  • 那么我们就需要修改了,修改成子模块为library的时候,我们才去依赖那两个模块,否则不依赖
  • 修改app下面的build.gradle
dependencies {
    implementation rootProject.ext.dependencies.publicImplementation
    if (!rootProject.ext.android.is_application) {
      implementation project(path: ':login')
      implementation project(path: ':member')
    }
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

以上就是一些基本问题,下面还有一些问题

1.6.6 比如我们在项目中需要知道当前项目是application还是library

  • 这个时候就需要我们创建给基础库了
  • 为什么需要基础库呢?因为我们每个模块都需要用到这些东西

  • 接下来问题来了,基础库创建好了,我们要怎么写才能让每个Module都去依赖它呢?
  • 我们先在config中定义一个other库,然后再build.gradle中通过each语法,遍历,依赖即可

  • 这样,我们以后就可以只改动一个地方,就好了
  • 接下来我们要处理参数传递的问题,我们需要将is_application参数传递给每个模块的buildConfig文件中去
  • 我们需要在我们basic模块下的build.config文件中,添加下面的代码,注意各种环境都要添加该属性
buildTypes {
  release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
    'proguard-rules.pro'
    buildConfigField 'boolean', 'is_application',
    rootProject.ext.android.is_application.toString()
  }
  advanced {
    buildConfigField 'boolean', 'is_application',
    rootProject.ext.android.is_application.toString()
  }
  debug {
    buildConfigField 'boolean', 'is_application',
    rootProject.ext.android.is_application.toString()
  }
}
  • 接着我们Make一下工程
  • 查找basic的buildConfig文件,发现属性已经添加好了

  • 接下来我们就可以在basic模块下面使用了
  • 注意这里我们需要在basic模块下的application下面声明一个is_application的静态变量
  • 这样才能让我们的每一个模块都能使用上这个属性
  • 如下
package com.example.basic
import android.app.Application
/**
 * <pre>
 * author: Jafar
 * date : 2020/11/11
 * desc :
 * </pre>
*/
class BaseApplication : Application() {
  companion object {
    const val is_application = BuildConfig.is_application
  }
  override fun onCreate() {
    super.onCreate()
  }
}
  • 当然,我们每一个模块不一定都能用上application,那么我们的baseActivity无疑是最合适的
package com.example.arouterproject
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
/**
 * <pre>
 * author: Jafar
 * date : 2020/11/11
 * desc :
 * </pre>
*/
class BaseActivity : AppCompatActivity() {
  companion object {
    const val is_application = BuildConfig.is_application
  }
  override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  }
}
  • 我们有时候,需要在模块里面编译的时候加载一些用到的类,但是打包的时候不需要,怎么设置呢?

  • 如上图所示,我们只需要创建一个文件夹,把Java类拷贝进去,然后在sourceSets中添加这样一行代码即可
  • 其实这样一行代码的意思就是声明我这个目录就是一个Java的文件夹
  • 这样做的好处就是,开发的时候,我们使用Application编译,使用临时的代码进行编译,但是呢?打包的时候,我们就不需要用到了,也就是当其变为library的时候,我们就不需要了。减少了无所谓的编译
  • 这种做法,就会防止我们因为不断地切换而产生的BUG
  • 解决了资源浪费的问题
  • 到此为止。我们就把细节都处理的差不多了

模块之间的跳转怎么做呢?手写路由即可!

1.7 Android组件化基础

当 App 项目复杂一定的程度,将项目组件化是必不可少的,组件化可以更好的进行功能的划分,提到组件化有人可能会想到模块化,其实组件化和模块化的本质是一样的,都是为了代码重用的业务解耦,模块化主要按照业务划分,而组件化主要按照功能划分,从组件化最基础的几个方面打开组件化的大门。

  1. 组件之间的跳转
  2. 动态创建
  3. 资源冲突
  4. 静态常量

1.7.1 组件之间的跳转

组件化中两个功能模块时不直接依赖的,其依赖规则是通过 Base module 间接依赖,当组件之间的 Activity 进行界面跳转时,由于没有相互依赖的关系,往往会无法引用另一个 module 中的 Activity。

隐式跳转

隐式跳转是通过 Android 原生 Intent 匹配机制来实现相应跳转,就是使用 Action 来跳转到对应的 Activity,这样使用隐式跳转的方式就可以跨 module 实现 Activity 之间的跳转了,注意一点,如果移出 Activity 所在的 module 而不移出相应的跳转,如果继续跳转会出现异常,使用隐式 Intent 跳转需要验证是否会接收该 Intent,需要对该 Intent 对象调用 resolveActivity() 方法来判断至少有一个应用能够处理该 Intent,通过隐式跳转的方式还可以设置 exported 为 false 来确保只有自己的 App 才能够启动对应的组件

ARouter跳转

Android 开发中可将 module 看成不同的网络,而对应的 Router 就是连接各个 module 的中转站,这个中转站可以对页面跳转的参数等进行统一处理,ARouter 是阿里开源出来的一个页面跳转路由,使用 ARouter 可以替代隐式跳转来完成不同 module、不同组件之间的跳转以及跳转过程的监听、参数的传递等,ARouter 支持路径跳转和 URL 跳转两种方式,使用也非常灵活,ARouter 的具体使用这里不做介绍,其具体使用会在单独一篇文章中详解,ARouter 与 Android 传统跳转方式的对比如下:

  1. 显示跳转需要依赖于类,而路由跳转通过指定的路径跳转;
  2. 隐式跳转通过 AndroidManifest 集中管理,导致协作开发困难;
  3. 原生使用 AndroidManifest 来注册,而路由使用注解注册
  4. 原生 startActivity 之后跳转过程交由 Android 系统控制,而路由跳转采用的是 AOP 切面编程可对跳转过程进行拦截和过滤。

动态创建

组件化开发中最重要的一点就是各个模块、各个组件之间要尽可能解耦,这样很容易就会想到使用 Java 中的反射机制,使用反射可在运行状态下获取某个类的所有信息,然后就可以动态操作这个类的属性和方法了。如果 Fragment 单独作为一个组件来使用时,当这个 Fragment 组件不需要被移出后,如果是常规的 Fragment 则会因为索引不到该 Fragment 而使得 App 崩溃,想一下如果使用反射创建 Fragment 的方式则至少不会引起 App 崩溃,这里可以捕捉异常完成相关逻辑,这样是不是降低了耦合呢。可见,虽然反射有一定的性能问题,但使用反射确实能在一定程度上降低耦合,学习组件化 Java 反射机制应该是必须的一部分。

组件化开发中要求每个组件都能独立运行,一般情况下每个组件都有一定的初始化步骤,最好的一种情况是项目需要的几个组件的初始化基本相同,那就可将初始化放在 BaseModule 中进行统一初始化,但是这种情况毕竟比较理想,一般情况是每个组件的初始化都不一样,可能你会想到在各自的 Application 初始化,如果在各自的Application 中初始化,当在最终编译由于 Application 的合并难免会出一些问题,这种方式也不可取,到这里又想到了反射,在各组件中创建初始化文件,然后在最终的 Application 中通过反射完成各个组件的初始化操作,这里通过 Java 的反射机制完成了组件化开发中 Application 的动态配置。

资源冲突

组件化开发过程中,如果 ModuleA 的 AmdroidManifest 文件中使用 Android:name 属性指定了相应的 Application,而主 App Module 的 AndroidManifest 文件中也使用 android:name 属性指定了相对应的 Application,此时就必须在 主App Module 的 AndroidManifest 文件中使用 tools:replace="android:name" 来解决冲突,使用 replace 属性表示该属性也就是在 标签下的 android:name 属性可在编译过程中被替换,这样根据AndroidManifest 文件替换规则最终指定的 Application 应该是 App Module 中的指定的 Application。

举一个例子,我在项目中的某个功能 Module 中使用 SMSSDK 来完成短信验证的功能,因为其他地方不用,所以只引入到了要使用的功能 Module 中,如果其他 Module 会使用应该将 SMSSDK 引入到 BaseModule 中,使用SMSSDK 如果不指定该 Module 的 Application,MobSDK 会将 com.mob.MobApplication 指定为该 Module 的 Application,此时在整体编译打包时就会出现 AndroidManifest 文件的 android:name 属性冲突,当然了解决方法就是使用 replace 属性了。 AndroidManifest 文件合并后的主要冲突也就是这个问题了,当然 下的其他属性有冲突,也是使用 replace 属性。在实际的开发中多验证会更有收获喔。

组件化开发中另外需要注意的一点是防止资源名称一样导致最终合并的时候,因为冲突造成资源引用错误或者某些资源丢失等,如字符串、颜色值等资源等合并的时候会被后面加载的相同名称的资源所替换,解决的思路是在资源命名上要有一定的规则,可以在 build.gradle 文件中配置 "resourcePrefix "组件名称"" 的方式强制约束开发者确保资源名称唯一,建议 Module 中资源的命名格式为 "Module名称功能其他"。

静态常量

组件化开发中,最终合并时每个组件都是以 Lib Module 的形式存在,而 Lib Module 中 R.java 文件中定义的静态变量没有声明为 final,这就意味着不能在组件 Module 中使用相对应的常量了,如在时候 switch 语句就不能使用了,这就要求在组件中要使用 if 语句来替代 switch 语句,当然在组件独立运行的时候是没有这个问题的。

开发中经常会使用到 Butterknife,Butterknife 可非常方便的对 View 及 View 的事件等进行注解操作,它采用的是编译时注解机制,注解中只能使用常量,所以在 Butterknife 在组件化开发中应该使用 R2 代替 R,R2 实际上是 R 的拷贝, R2 对应声明的变量是 final,所以在组件化开发中如果使用 Butterknife 在相应的注解中要使用 R2 替代 R。