写一个MVVM快速开发框架(二)组件化改造

A succinct hexo theme...

Posted by 卢小胖 on 2021-08-10
Estimated Reading Time 19 Minutes
Words 4.2k In Total
Viewed Times

组件化思想

组件化对于各个领域都已经不是新鲜的词汇了,在Android上组件化已经遍地开花了,这里只是记录一下我对组件化的理解,有不对的地方请大家指出。

组件化是为了解决什么问题

优秀的工程项目应该满足高内聚低耦合思想,各个功能有明显的边界划分,各个模块各司其职,至少在修改的时候不是牵一发而动全身,其他人在接手的时候也能快速理解。

如果你的项目存在一下问题,可以考虑使用组件化了:

  1. 代码耦合严重,eventbus满天飞
  2. 依赖严重,编译慢
  3. 功能模块界限不清晰
  4. 多人开发的时候经常发生合并冲突

组件化的存在就是为了解决上述问题,将功能相关的作为一个单独模块,将经常使用的基础功能抽离出来作为基础模块,多人开发时每个人负责独立模块。

理想状态下,每个模块能进行独立开发、独立测试,在打包时合并为一个项目。

怎样使用组件化

大致分为以下步骤:

  1. 解决依赖问题
  2. 抽离公用模块
  3. 创建独立功能模块
  4. 模块间通信

我们的目的是搭建MVVM模式下的组件化框架,关于组件化的理论知识我们不做过多的介绍了。

参考:
https://www.jianshu.com/p/5e20a674062f
https://juejin.cn/post/6881116198889586701

搭建

解决依赖问题

组件化分为宿主App+多module,如果每个moduel都使用自己的依赖,那么管理起来相当费劲,不同module之间依赖的版本不同、升级都是问题,所以我们首先寻求的就是统一依赖管理

方式一:

在根目录下创建config.gradle文件,这个文件就是管理常用配置和依赖:

ext{

versions = [
compileSdkVersion: 29,
buildToolsVersion: "29.0.2",
minSdkVersion : 21,
targetSdkVersion : 29,
versionCode : 1,
versionName : "1.0"
]

// 组件化与集成化切换时,设置不同的applicationId
appId = [
app : "com.example.modular.todo",
shop: "com.example.modular.shop"
]


dependencies = [
appcompat : "androidx.appcompat:appcompat:${appcompatVersion}",
constraintlayout: "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}",
]


//这里只做实例

}

然后再根目录的build.gradle中添加:

apply from: 'config.gradle'

在不同的module模块中的引用:

// build.gradle

def supports = rootProject.ext.dependencies

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

implementation supports.appcompat
implementation supports.constraintlayout

// supports 依赖
supports.each { key, value -> implementation value }

}

还有一种buildSrc管理,我并不常用这种方式,更习惯下面这种:

最终实现方式

1. 新建moduel模块,名称为version
2. 将version模块的build.gradle修改为如下:
buildscript {
ext.kotlin_version = "1.5.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'

repositories {
google()
mavenCentral()
}

dependencies {
implementation gradleApi()
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

compileKotlin {
kotlinOptions {
jvmTarget = 1.8
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = 1.8
}
}

gradlePlugin {
plugins {
version {
// 在 app 模块需要通过 id 引用这个插件 ,修改为version模块包名
id = 'com.xlu.version'
// 实现这个插件的类的路径
implementationClass = 'com.xlu.version.VersionPlugin'
}
}
}

在此模块下创建VersionPlugin类,如下:


import org.gradle.api.Plugin
import org.gradle.api.Project

/**
* TODO 这里什么都不用做,创建就行了
*/
class VersionPlugin : Plugin<Project>{
override fun apply(target: Project) {

}
}
3. 重要的一步:
  • 打开工程根目录下的setting.gradle文件
  • 删除原来的include ':version'
  • 修改为includeBuild("version")
4. 在version模块下创建Dependency类,管理依赖

当然这个类名称自己随便起,实例如下:

object Dependency  {

object ProjectConfig {
const val compileSdkVersion = 30
const val buildToolsVersion = "30.0.1"
const val applicationId = "com.example.mvvm_develop"
const val minSdkVersion = 16
const val targetSdkVersion = 30

const val versionCode = 1
const val versionName = "1.0"
const val isAppMode = false

}

object Version{
// Android---------------------------------------------------------------
const val Junit = "4.13"
const val Material = "1.2.0" // 材料设计UI套件

// AndroidX--------------------------------------------------------------
const val AppCompat = "1.2.0"
const val CoreKtx = "1.3.1"
const val ConstraintLayout = "2.0.1" // 约束布局
const val TestExtJunit = "1.1.2"
const val TestEspresso = "3.3.0"
const val ActivityKtx = "1.1.0"
const val FragmentKtx = "1.2.5"
const val MultiDex = "2.0.1"

// Kotlin----------------------------------------------------------------
const val Kotlin = "1.5.10"
const val Coroutines = "1.5.0" // 协程

// JetPack---------------------------------------------------------------
const val Lifecycle = "2.3.1" // Lifecycle相关(ViewModel & LiveData & Lifecycle)
const val Hilt = "2.35.1" // DI框架-Hilt
const val HiltAndroidx = "1.0.0"
}

object DependencyImp{
//Android
const val Junit = "junit:junit:${Version.Junit}"
const val Material = "com.google.android.material:material:${Version.Material}"

//AndroidX
const val AndroidJUnitRunner = "androidx.test.runner.AndroidJUnitRunner"
const val AppCompat = "androidx.appcompat:appcompat:${Version.AppCompat}"
const val CoreKtx = "androidx.core:core-ktx:${Version.CoreKtx}"
const val ConstraintLayout = "androidx.constraintlayout:constraintlayout:${Version.ConstraintLayout}"
const val TestExtJunit = "androidx.test.ext:junit:${Version.TestExtJunit}"
const val TestEspresso = "androidx.test.espresso:espresso-core:${Version.TestEspresso}"
const val ActivityKtx = "androidx.activity:activity-ktx:${Version.ActivityKtx}"
const val FragmentKtx = "androidx.fragment:fragment-ktx:${Version.FragmentKtx}"
const val MultiDex = "androidx.multidex:multidex:${Version.MultiDex}"

//Kotlin
const val Kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Version.Kotlin}"
const val CoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.Coroutines}"
const val CoroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.Coroutines}"

//Jetpack
const val ViewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Version.Lifecycle}"
const val ViewModelSavedState = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Version.Lifecycle}"
const val LiveData = "androidx.lifecycle:lifecycle-livedata-ktx:${Version.Lifecycle}"
const val Lifecycle = "androidx.lifecycle:lifecycle-runtime-ktx:${Version.Lifecycle}"
const val LifecycleCompilerAPT = "androidx.lifecycle:lifecycle-compiler:${Version.Lifecycle}"
const val HiltCore = "com.google.dagger:hilt-android:${Version.Hilt}"
const val HiltApt = "com.google.dagger:hilt-compiler:${Version.Hilt}"
const val HiltAndroidx = "androidx.hilt:hilt-compiler:${Version.HiltAndroidx}"
}

}

上面代码应该很好理解吧,我们在上述创建好我们常用的依赖之后,就可以在其他modue中引用了。

上面类中的依赖在编译时并不会创建,只有在module中引用才会创建。

5. 其它module中使用

我们以宿主app模块为例,打开app模块的build.gradle文件


//setup1
import com.example.plugin.Dependency

// setup2
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.example.plugin' // 注意这里哦,不然上述的import会报错
}

一些配置基础:

android {
compileSdkVersion Dependency.ProjectConfig.compileSdkVersion
buildToolsVersion Dependency.ProjectConfig.buildToolsVersion

defaultConfig {
applicationId Dependency.ProjectConfig.applicationId
minSdkVersion Dependency.ProjectConfig.minSdkVersion
targetSdkVersion Dependency.ProjectConfig.targetSdkVersion
versionCode Dependency.ProjectConfig.versionCode
versionName Dependency.ProjectConfig.versionName

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

......

}

依赖管理:


dependencies {

testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

//Android
api Dependency.DependencyImp.AppCompat
api Dependency.DependencyImp.Material
api Dependency.DependencyImp.AndroidXLeagcy
api Dependency.DependencyImp.ActivityKtx
api Dependency.DependencyImp.FragmentKtx
api Dependency.DependencyImp.MultiDex
//Kotlin
api Dependency.DependencyImp.Kotlin
api Dependency.DependencyImp.CoreKtx
//Kotlin协程核心库
api Dependency.DependencyImp.CoroutinesCore
api Dependency.DependencyImp.CoroutinesAndroid
//navigation
api Dependency.DependencyImp.NavigationFragment
api Dependency.DependencyImp.NavigationUI
//lifecycle
api Dependency.DependencyImp.Lifecycle
api Dependency.DependencyImp.Livedata
api Dependency.DependencyImp.Viewmodel

}

具体参考Demo

上面可以看到我们用Dependency.ProjectConfig.xx这种方式代替了原来的数据,在Dependency类中统一管理

注意version所在的位置应该与Project处于同一层级,而不是与app module处于同一层级:

不然MakeProjectk可能会报错:

Project directory 'C:xxx\version' is not part of the build defined by settings file 'C:xxx\project\settings.gradle'.   
If this is an unrelated build, it must have its own settings file.

setting.gradle配置如下:

include ':module_home'
include ':module_login'
include ':base'
include ':app'
includeBuild "../version"
rootProject.name = "mvvm_develop"

具体参考Demo

我这边升级了2020.03版本AndroidStudio,用的kotlin1.5.20, Gradle Version 7.0.2,上述有些使用jcenter的依赖需要注意下,新版AS已近完全抛弃了Jcenter

以上就完成了组件化的配置

参考资料
https://blog.csdn.net/yubo_725/article/details/118895551


抽离公用模块

有些功能是所有模块能公用的,比如:

  • 网络请求
  • 图片加载
  • 文件操作
  • 权限操作
  • 网页加载

这些模块一般不会去改动,和业务没有关联,但是所有模块都能公用,我们可以将其封装在base基础module中。

如果想将这个模块应用到多个项目上,可以单独发在公司的maven仓库中,也可以使用jitpack

上传到maven可以参考:https://blog.csdn.net/wang295689649/article/details/105219490
如何将单独的module发布到jitpack,参考:https://www.jianshu.com/p/b7552cf8983b,官方文档:https://jitpack.io/docs/ANDROID/

模块间通信

我们这里使用ARouter进行模块间的通信,对ARouter熟悉的同学就可以直接跳过了

基础接入:

在同一依赖管理中配置好:


const val ARoute = "1.5.2" // 阿里路由
const val ARouteCompiler = "1.5.2" // 阿里路由 APT


const val ARoute = "com.alibaba:arouter-api:${Version.ARoute}"
const val ARouteCompiler = "com.alibaba:arouter-compiler:${Version.ARouteCompiler}"

然后需要在所有使用了ARouter的模块中进行配置,注意java版本和kotlin有些不一样:

Java:

defaultConfig {

...

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


dependencies {

...

implementation Dependency.DependencyImp.ARoute
annotationProcessor Dependency.DependencyImp.ARouteCompiler

}

Kotlin:

defaultConfig {

...

kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}


dependencies {

...

implementation Dependency.DependencyImp.ARoute
kapt Dependency.DependencyImp.ARouteCompiler
}

在公共模块中统一管理路径,我这里创建了一个Constants类去同一管理路径:

object ARouterConstant {

const val MainActivity = "/app/MainActivity"

const val ARouterFragment = "/app/ARouterFragment"

const val LoginActivity = "/login/LoginActivity"
}

注意路径至少需要两级,建议使用/module name/class name这样子配置

页面跳转

在activity上或者fragment上配置:

@Route(path = ARouterConstant.LoginActivity)
class LoginActivity : BaseActivity(R.layout.activity_login) {

}

@Route(path = ARouterConstant.ARouterFragment)
class ARouterFragment : BaseFragment(R.layout.fragment_arouter) {

}
基础跳转:
//跳转到Login模块 -> LoginActivity
ARouter.getInstance().build(ARouterConstant.LoginActivity).navigation()
携带参数跳转:
ARouter.getInstance().build(ConstantARouter.LoginActivity)
.withLong(ConstantParams.key1, 666L)
.withString(ConstantParams.key2, "888")
.navigation()

在目的页面获取参数:

private fun initParams() {
val key1 = intent?.getLongExtra(ConstantParams.key1,0L)
val key2 = intent?.getStringExtra(ConstantParams.key2)
}

可以使用@Autowired注解自动注入,如下:


@Autowired
@JvmField var key1 : Long = 0L


@Autowired(name = ConstantParams.key2)
@JvmField var test : String = ""


override fun initData(savedInstanceState: Bundle?) {

// Start auto inject.
ARouter.getInstance().inject(this)

}

使用@Autowired注解的时候需要配合@JVMField使用,在初始化的时候调用ARouter.getInstance().inject(this)

如果需要传递自定义对象,可以实现Serizable接口:

我们自定义一个数据bean:
data class RouterBeanSerializable(val id:Int, val name:String):Serializable


传递:
ARouter.getInstance()
.build(ConstantARouter.LoginActivity)
.withSerializable(ConstantParams.key3,RouterBeanSerializable(id = 123,name = "哈哈哈哈"))
.navigation()

获取:
val key3 = intent?.getSerializableExtra(ConstantParams.key3)


输出结果:
key3:RouterBeanSerializable(id=123, name=哈哈哈哈)

对于没有实现序列化接口的对象,官方提供了withObject接口,我感觉没必要,有兴趣的可以参考这篇文章

ARouter获取Fragment实例:

ARouter是不支持fragment跳转的,只能获取fragment实例:

val fragment: Fragment = ARouter.getInstance().build(ConstantARouter.ARouterFragment)
.withLong("id", key1)
.withString("name", test)
.navigation() as Fragment

更多关于更多ARouter的用法便不再阐述,请查看官方文档,我在Demo中也有进行演示

数据通信

上面介绍了使用ARouter进行页面跳转,和简单的页面间数据传递。如果我们不进行页面跳转,多module间有哪些数据通信的方式?

进行数据通信的方式有EventBus,数据持久化,进程间通信。大多不适用于我们的场景,我这里采用的是大佬推荐的向外暴露接口的方式。

我们新建common module,这个module主要是提供业务相关的公共依赖,我们在这里实现数据接口调用。

我们让common module使用api project(path: ':base')
这样其他模块只需要依赖一个common就行了

这样做的好处是隔离每个module,不用使各个module之间相互依赖才能调用对方的服务

这里以login module为例,提供获取用户信息的接口,先创建LoginInterface接口:

interface LoginInterface {

fun getToken():String

fun getUserName():String

fun getUserID():Int

}

ARouter中提供了IProvider接口服务,我们需要让自定义接口继承IProvider接口。

interface LoginInterface : IProvider

接下来只需要在login module中实现此接口就行了:

这里只是一些模拟数据

class LoginImpl : LoginInterface {

override fun init(context: Context?) {
//此方法为IProvider接口提供
}

override fun getToken(): String {
return "this is login token"
}

override fun getUserName(): String {
return "name"
}

override fun getUserID(): Int {
return 123
}

}

我们可以创建一个工具类去统一管理:

object LoginUtil {

fun getLoginServer():LoginInterface{
return ARouter.getInstance().navigation(LoginInterface::class.java)//如果只有一个实现,这种方式也可以
//return ARouter.getInstance().build(ConstantARouter.LoginItfImpl).navigation() as LoginInterface
}

}

在其他module中调用:

val token:String = getLoginServer().getToken()

到此我们初步完成了组件化的搭建,更多使用的坑大家自行探索吧哈哈哈哈

参考:
https://juejin.cn/post/6881116198889586701
https://juejin.cn/post/6844903649102004231
https://juejin.cn/post/6844903687488274445、

上面三篇文章都挺值得大家参考的,感谢大佬们的付出

关于一些奇怪的问题

Moduel独立调试

moduel独立调试的时候需要设置不同的mainfest.xml文件,还要配置applicationId。

步骤如下:

1. 在Version模块中配置参数

object Dependency  {

//是否允许module独立允许
object RunAlone{
const val base = false
const val home = true //以home独立允许为例
const val login = false
const val jitpack = false
}


....

}

2.在home moduel中新建Mainfest.xml文件

作为独立app的Mainfest文件个作为module的Mainfest文件是不一样的,我们需要创建两个

3.配置mdule_home的build.gradle文件

import com.xlu.version.Dependency

plugins {
id 'com.xlu.version'
}

//setup1:
if (Dependency.RunAlone.home.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
compileSdkVersion Dependency.ProjectConfig.compileSdkVersion
buildToolsVersion Dependency.ProjectConfig.buildToolsVersion

defaultConfig {
//setup2:作为独立app需要配置applicationId
if (Dependency.RunAlone.home.toBoolean()) {
applicationId Dependency.ProjectConfig.applicationId_home
}

......
}

......

//setup3:
sourceSets {
main {
// 独立调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (Dependency.RunAlone.home.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}

dependencies {
implementation project(path: ':base')

}

上述3步已经标明了该如何配置。

Application生命周期

我们通常在Application中执行一些初始化操作,比如sdk初始化。但是只有能独立运行的module才能拥有Application

如果我们将所有的初始化操作都方法在app application中,就会造成每次子module引入新的库都需要修改app module,有没有办法让子module自己执行初始化。

方式一:

反射实现,参考:https://juejin.cn/post/6844903649102004231#heading-12

方式二

生命周期分发,参考:
https://www.jianshu.com/p/59368ce8b670
https://juejin.cn/post/6881116198889586701#heading-26

方式三:

我们上面提到了其他module想外暴露服务,采用面向接口编程的方式,其他模块不能设计具体的实现类,我们在这里同样可以借鉴,Google提供了AutoSerive服务,一个简单的例子:

新建一个接口:

interface Display{

fun show()

}

有很多的类实现了Display接口:


class phone :Diaplay{

override fun show(){
//show phone
}
}


class monitor :Diaplay{

override fun show(){
//show monitor
}
}

我们需要在所有的实现类上加上注解:@AutoService(Display::class)

我们可以用过ServiceLoader加载出此接口所有的实现类

val mLoader: ServiceLoader<Display> = ServiceLoader.load(Display::class.java)

mLoader.forEach {
it.show()
}

就是这么的简单,我们可以通过@AutoService来实现,这个我们的application初始化有什么关系呢?

  1. 创建相关接口:

interface ApplicationLifecycle {

fun onAttachBaseContext(context: Context)

fun onCreate(application: Application)

fun onTerminate(application: Application)

fun init()
}
  1. 创建一个代理类来实现此接口:
//注意这里不需要注解@AutoService
class LoadModuleProxy : ApplicationLifecycle {

private var mLoader: ServiceLoader<ApplicationLifecycle> =
ServiceLoader.load(ApplicationLifecycle::class.java)


override fun onAttachBaseContext(context: Context) {
mLoader.forEach {
Log.d("ApplicationInit", it.toString())
it.onAttachBaseContext(context)
}
}

override fun onCreate(application: Application) {
mLoader.forEach { it.onCreate(application) }
}

override fun onTerminate(application: Application) {
mLoader.forEach { it.onTerminate(application) }
}

override fun init(){
mLoader.forEach { it.init() }
}

}
  1. 在BaseApplication中注册:

open class BaseApp :Application() {

private val mLoadModuleProxy by lazy(mode = LazyThreadSafetyMode.NONE) { LoadModuleProxy() }

companion object{
private lateinit var baseApplication: BaseApp

fun getContext(): Context {
return baseApplication
}
}

override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
mLoadModuleProxy.onAttachBaseContext(base)
}

override fun onCreate() {
super.onCreate()
baseApplication = this

mLoadModuleProxy.onCreate(this)
}

override fun onTerminate() {
super.onTerminate()
mLoadModuleProxy.onTerminate(this)
}

}
  1. 在子moduel中Application中实现ApplicationLifecycle接口

@AutoService(ApplicationLifecycle::class)
class AppHome : ApplicationLifecycle {

private val TAG = "AppHome"

override fun onAttachBaseContext(context: Context) {
}

override fun onCreate(application: Application) {
}

override fun onTerminate(application: Application) {
}

override fun init(){
initSdkWorker()
initSdkMain()
}

private fun initSdkWorker() {
xLog.d(TAG, "initSdkWorker: ")
}

private fun initSdkMain() {
xLog.d(TAG, "initSdkMain: ")
}

}

以上就可以实现子module自己愉快的玩耍了,AutoService使用参考:https://www.jianshu.com/p/086fe09188ea