单工程遇到的问题
随着项目逐渐发展,业务越来越多,代码量也越来越多,耦合严重,层次混乱,页面互相之间的跳转有着极强的关联性,所有代码都写在app module中,编译一次都要5-6分钟,为了方便以后项目的开发/测试以及提高编译性能就需要进行组件化了。
组件化的优势
- 降低耦合度:每个业务模块无不关联,可自由拆卸、自由组装,重复利用
- 加快编译速度:每个组件可以单独编译运行,发布时也可以合并成一个app。
- 提高协作效率:团队中每个人负责自己的组件,不会影响其他组件,降低团队成员熟悉项目的成本。
组件如何划分
我们可以分为基础组件、Common组件、业务基础组件、业务组件、注册登录组件、app壳组件。
- 基础组件:最基础的,一般不会怎么变化的功能,比如网络请求、图片加载、日志、工具类、权限等。
- Common组件:服务暴露接口、自定义view、部分共用实体类、部分共用资源文件、各种Base基类(BaseActivity/BaseFragment等),依赖基础组件。
- 业务基础组件:比如分享、推送等功能,依赖基础组件。
- 业务组件:项目的各个业务模块,我们日常主要围绕着业务组件开发,依赖Common组件。
- app壳组件:原则上不包含业务代码,依赖Common组件和业务组件。
- 注册登录组件:依赖Common组件,业务组件、app壳组件按需依赖该组件。
项目结构图
组件单独调试、gradle配置
统一配置依赖
项目划分了组件需要对每个组件进行统一配置,否则版本不一致会出现各种问题。首先需要在项目根目录创建一个config.gradle文件,然后统一定义版本、依赖等,最后在各个组件中的build.gradle中使用config.gradle定义的版本、依赖。
动态配置插件
现在项目都是gradle构建的,gradle提供了两种插件用来配置不同的工程
- com.android.application:app插件
- com.android.library:library插件
app插件配置android app工程,项目构建后会打包成apk包。library插件用来配置android library工程,项目构建后会打包成aar包。我们需要动态配置这两种插件,当需要发布包时需要配置library插件,当开发单独调试的时候需要配置app插件。
我们需要gradle.properties文件中定义一个isRelease = false
变量,当为true的时候不能单独运行,当为false的时候可以独立运行。
动态配置ApplicationId和AndroidManifest
一个app要想独立运行需要一个ApplicationId的,一个app也只有一个ApplicationId,所以在单独调试和集成调试时组件的 ApplicationId 应该是不同的。一个app需要有一个启动页,集成调试的时候AndroidManifest会合并成一个,因此需要配置两个AndroidManifest,一个有启动页一个没有启动页,根据isRelease来动态切换。
//单独调试
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jk.order">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CustomARouter">
<activity
android:name=".OrderActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
//集成调试
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jk.order">
<application>
<activity android:name=".OrderActivity" />
</application>
</manifest>
组件之间页面跳转
组件化的核心解耦,组件之间无不关联,相互之间不能有引用,那么怎么实现组件之间的跳转呢?比较著名的有两款框架,分别是阿里的ARouter和美团的WMRouter,他们的原理类似,都是基于apt技术动态生成代码,实现可以根据配置的路由信息进行跳转、数据共享、拦截器等。ARouter功能更加强大,使用人数也更多,因此我们选择ARouter作为路由框架,具体的使用方法参考ARouter文档。
组件之间数据共享
由于组件之间不能相互引用,那如果B组件要用到A组件中的数据该怎么办呢?那么我们可以通过Arouter中的暴露服务。在module_common组件中定义接口,然后在需要暴露数据的组件中实现该接口并定义路由,这样就可以在其他组件调用到该组件的数据了。
//common组件IBuildConfigProvider
interface IBuildConfigProvider : IProvider {
fun getApplicationId(): String
fun getApiHost(): String
fun getBaseUrl(): String
fun getGhzsUrl(): String
fun getVersionName(): String
fun getFlavor(): String
}
//A组件BuildConfigImpl
@Route(path = RouteConsts.provider.buildConfig, name = "BuildConfig暴露服务")
class BuildConfigImpl : IBuildConfigProvider {
override fun getApplicationId(): String = BuildConfig.APPLICATION_ID
override fun getApiHost(): String = BuildConfig.API_HOST
override fun getBaseUrl(): String = BuildConfig.BASE_URL
override fun getGhzsUrl(): String = BuildConfig.GHZS_URI
override fun getVersionName(): String = BuildConfig.VERSION_NAME
override fun getFlavor(): String = BuildConfig.FLAVOR
override fun init(context: Context?) {
}
}
//在B组件中使用依赖查找的方式发现服务
val buildConfig = ARouter.getInstance().build(RouteConsts.provider.buildConfig).navigation() as? IBuildConfigProvider
Application生命周期分发
我们通常会在Application的onCreate中做一些初始化任务,而业务组件有时也需要获取应用的Application,也要在应用启动时进行一些初始化任务。直接在壳工程Application的onCreate操作就可以啊,但是这样做会带来问题:因为我们希望壳工程和业务组件代码隔离,并且我们希望组件内部的任务要在业务组件内部完成。那么如何做到各业务组件无侵入地获取 Application生命周期呢?答案是使用spi技术,它可用于在Android组件化开发中Application生命周期主动分发到组件。具体使用如下:
引入依赖:
//common组件 build.gradle
api 'com.google.auto.service:auto-service:1.0-rc7'
kapt 'com.google.auto.service:auto-service:1.0-rc7'
然后在Common组件中创建一个接口,这个接口和Application中的接口方法一致:
interface IApplication {
fun attachBaseContext(base: Context)
fun onCreate()
fun onLowMemory()
fun onTerminate()
fun onTrimMemory(level: Int)
fun onConfigurationChanged(newConfig: Configuration)
}
接下来就在各个组件中实现IApplication接口:
@AutoService(IApplication::class)
class ApplicationImpl : IApplication {
override fun attachBaseContext(base: Context) {
app = base as Application
}
override fun onCreate() {
}
override fun onLowMemory() {
}
override fun onTerminate() {
}
override fun onTrimMemory(level: Int) {
}
override fun onConfigurationChanged(newConfig: Configuration) {
}
companion object {
lateinit var app: Application
}
}
最后就在app模块中使用ServiceLoader加载出每个模块实现的ApplicationImpl,Application执行每个生命周期的时候分发给每个组件:
class App : Application() {
private var mApplicationList: List<IApplication> = ServiceLoader.load(IApplication::class.java, javaClass.classLoader).toList()
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
mApplicationList.forEach {
it.attachBaseContext(this)
}
}
override fun onCreate() {
super.onCreate()
mApplicationList.forEach {
it.onCreate()
}
}
override fun onLowMemory() {
super.onLowMemory()
mApplicationList.forEach {
it.onLowMemory()
}
}
override fun onTerminate() {
super.onTerminate()
mApplicationList.forEach {
it.onTerminate()
}
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
mApplicationList.forEach {
it.onTrimMemory(level)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
mApplicationList.forEach {
it.onConfigurationChanged(newConfig)
}
}
}
这样就实现了Application生命周期分发,构建app包,查看META-INF/services目录会生成一个文件,这个文件包含了我们在各个模块实现的IApplication类的全类名:
资源冲突
整个项目中尽量不要出现重复的资源,否则会出现资源冲突问题,如果一个资源很多模块都要用到,需要放到common组件中。网上都是推荐在gradle文件中配置 resourcePrefix "moudle_prefix"
,但是感觉不太好,这样会导致重复资源打包进apk,导致包体积增大。
总结
本文介绍了单工程带来的问题、组件化的优势、组件的配置/调试、组件之间的数据共享、资源冲突、以及通过SPI技术实现Application生命周期分发等,本文提到的问题基本上就是组件化大部分所面对的问题。