Anroid组件化构架设计

发布时间:2023-04-03 10:30

为什么需要组件化

  • 极大提高工程编译速度
  • 业务模块解耦,有利于多人团队协作开发

什么是组件化

所谓的组件化就是把需求拆成一个一个的小模块,最后组装需要的模块组成一个app

模块化架构

创建一个 Project 后可以创建多个 Module,这个 Module 就是所谓的模块。一个的例子,在写代码的时候我们会把每个模块拆开,每个 tab 所包含的内容就是一个模块,这样可以减少 module 的代码量,但是每个模块之间的肯定是有页面的跳转,数据传递等。

组件化架构

\"Anroid组件化构架设计_第1张图片\"

组件化带来的优势

加快编译速度:每个业务组件都可以单独运行调试,速度提升好几倍

功能重用:一次编码处处复用,再也不需要复制代码了。基础组件和业务基础组件,调用者可以根据文档就可以一键集成和使用。

提高协作效率:每个组件都有专人维护,只需要重点测试修改的组件即可。

组件化配置

1、全局配置

全局配置 :

isModuleMode 是最终要的配置 , 通过该配置的 true / false 设置当前是否开启组件化

  • 集成模式 true ( 默认模式 , 模块化 )
  • 组件模式 false ( 组件化 )

androidConfig 用于统一管理各个 Module 中的版本号 , 如编译版本号 , 最小版本号 , 目标版本号 ;applicationId 用于保存各个模块的包名 , 尤其是 module 依赖库的包名 , 组件化的状态下 , 该 module 需要独立运行 , 必须配置一个 applicationId 包名 。

dependencies 用于统一管理各个模块之间的依赖库 , 避免管理分散 ;

// ext 是 extension 扩展的含义
// ext 后的 {} 花括号 , 是闭包 ,
ext{

    // 是否是模块化模式
    // 集成模式 true ( 默认模式 , 模块化 )
    // 组件模式 false ( 组件化 )
    isModuleMode = true

    // 定义 android 变量 , 类型是字典 Map 集合
    // 其中定义了若干键值对集合
    androidConfig = [
            compileSdkVersion : 30,
            minSdkVersion : 18,
            targetSdkVersion : 30,
            versionCode : 1,
            versionName : \"1.0\"
    ]

    applicationId = [
            \"app\" : \"kim.hsl.component\",
            \"module1\" : \"kim.hsl.module1\",
            \"module2\" : \"kim.hsl.module2\",
    ]

    // androidx 版本号
    androidxVersion = \"1.3.0\"
    constraintlayoutVersion = \"2.0.4\"
    materialVersion = \"1.3.0\"

    // 统一管理依赖库
    dependencies = [
            // ${} 表示引用之前定义的变量
            \"appcompat\" : \"androidx.appcompat:appcompat:${androidxVersion}\",
            \"constraintlayout\" : \"androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}\",
            \"material\" : \"com.google.android.material:material:${materialVersion}\"
    ]
}

2、工程下的 build.gradle 配置

在总的 build.gradle 配置中 , 引入上述全局配置 , 其作用就相当于将上述全局配置原封不动拷贝过来 ;

apply from: \"component.gradle\"

完整配置 :

// Top-level build file where you can add configuration options common to all sub-projects/modules.
// 将 component.gradle 配置文件中的内容导入到该位置
// 相当于引入头文件
apply from: \"component.gradle\"

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath \"com.android.tools.build:gradle:4.1.0\"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

3、app 模块下的 build.gradle 配置

在 app 模块中重点关注 , 在组件模式下 , 一定不能引入依赖库 , 否则会报错 , 因为组件模式下这两个依赖库是两个可运行的独立应用 ;

dependencies {
    if (isModuleMode){
        // 集成模式下才能引用这两个 Library Module
        implementation project(path: \':module1\')
        implementation project(path: \':module2\')
    }
}

版本号 , applicationId , 依赖库 统一管理 :

从 Project 级别的配置中获取变量 :

def androidConfig = rootProject.ext.androidConfig
def appId = rootProject.ext.applicationId
def dep = rootProject.ext.dependencies

版本号 和 applicationId 统一管理 :

android {
    compileSdkVersion androidConfig.compileSdkVersion

    defaultConfig {
        applicationId appId[\"app\"]
        minSdkVersion androidConfig.minSdkVersion
        targetSdkVersion androidConfig.targetSdkVersion
        versionCode androidConfig.versionCode
        versionName androidConfig.versionName
    }

依赖库统一管理 :

dependencies {

    //implementation \'androidx.appcompat:appcompat:1.3.0\'
    //implementation \'androidx.constraintlayout:constraintlayout:2.0.4\'
    //implementation \'com.google.android.material:material:1.3.0\'

    implementation dep.appcompat
    implementation dep.constraintlayout
    implementation dep.material
}

完整配置 :

plugins {
    id \'com.android.application\'
}

def androidConfig = rootProject.ext.androidConfig
def appId = rootProject.ext.applicationId
def dep = rootProject.ext.dependencies

android {
    compileSdkVersion androidConfig.compileSdkVersion
    buildToolsVersion \"30.0.3\"

    defaultConfig {
        applicationId appId[\"app\"]
        minSdkVersion androidConfig.minSdkVersion
        targetSdkVersion androidConfig.targetSdkVersion
        versionCode androidConfig.versionCode
        versionName androidConfig.versionName

        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"

        // ARoute 需要的配置
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile(\'proguard-android-optimize.txt\'), \'proguard-rules.pro\'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    //implementation \'androidx.appcompat:appcompat:1.3.0\'
    //implementation \'androidx.constraintlayout:constraintlayout:2.0.4\'
    //implementation \'com.google.android.material:material:1.3.0\'

    implementation dep.appcompat
    implementation dep.constraintlayout
    implementation dep.material

    testImplementation \'junit:junit:4.+\'
    androidTestImplementation \'androidx.test.ext:junit:1.1.2\'
    androidTestImplementation \'androidx.test.espresso:espresso-core:3.3.0\'

    // 替换成最新版本, 需要注意的是api
    // 要与compiler匹配使用,均使用最新版可以保证兼容
    api \'com.alibaba:arouter-api:1.5.1\'
    annotationProcessor \'com.alibaba:arouter-compiler:1.5.1\'

    if (isModuleMode){
        // 集成模式下才能引用这两个 Library Module
        implementation project(path: \':module1\')
        implementation project(path: \':module2\')
    }

}

组件化需解决的问题

  • 业务组件间没有依赖,如何实现数据通信?
  • 业务组件如何实现单独调试?
  • 壳工程Application生命周期如何下发?
  • 业务组件间没有依赖,如何实现页面跳转?

独立调试

单工程方案

所谓的单工程方案就是把所有组件都放到一个工程下

单工程利弊分析:

  • 利:一个模块修改后只需要编译一下
  • 弊:没能做到完全的职责拆分,每个模块的开发者都有修改其他模块的权限。

首先在 gradle.properties 文件内声明一个变量:

// gradle.properties
isModule = true

isModule 为 true 时表示组件可以作为 apk 运行起来,false 表示组件只能作为 library。我们根据需要改变这个值后同步下gradle即可。

然后在某个 module 的 build.gradle 文件内用这个变量做三个地方的判断:

// build.gradle

// 区分是应用还是库
if(isModule.toBoolean()) {
    apply plugin: \'com.android.application\'
}else {
    apply plugin: \'com.android.library\'
}

android {
    defaultConfig {
        // 如果是应用需要指定application
        if(isModule.toBoolean()) {
            applicationId \"com.xxx.xxx\"
        }
    }
    sourceSets {
        main {
            // 应用和库的AndroidManifest文件区分
            if(isModule.toBoolean()) {
                manifest.srcFile \'src/main/debug/AndroidManifest.xml\'
            }else {
                manifest.srcFile \'src/main/AndroidManifest.xml\' 
            }
        }
    }
}

由于library是不需要 Application 和启动Activity页,所以我们要区分这个文件,应用manifest指定的路径没有特定,随意找个路径创建即可。在应用AndroidManifest.xml里我们要设置启动页:


    
        
            
                
                
            
        
    

library 的 AndroidManifest.xml 不需要这些:



gradle 依赖 module 的方式主要有两种:

  • implementation: A implementation B,B implementation C, 但 A 不能访问到 C 的东西。
  • api:A api B,B api C,A能访问到C的东西。

一般来说我们只需要使用 implementation 即可,api 是会造成项目编译时间变长,而且会引入该模块不需要的功能,代码之间耦合变得严重了。不过 module_common 是统一了基础组件版本的公共库,所有组件都应需要依赖它并拥有基础组件的能力,所以基本每个业务组件和业务基础组件都应该依赖公共库:

dependencies {
    implementation project(\':module_common\')
}

而 common 组件依赖基础组件应该是用 api,因为把基础组件的能力传递给上层业务组件:

dependencies {
    api project(\':module_base\')
    api project(\':module_util\')
}

多工程方案

多工程就是每个组件都是一个工程,例如创建一个工程后 app 作为壳组件,它依赖 biz_home 运行,因此不需要 isModule 来控制独立调试,它本身就是一个工程可以独立调试。

多工程的利弊就是和单工程相反的:

  • 利:做到职责完全拆分,其他项目复用更加方便,直接一行依赖引入。
  • 弊:修改后需要上传到maven仓库,其他工程再次编译后才能感知到变化,多了上传和编译的时间。

多工程组件依赖需要用到maven仓库。把每个组件的aar上传到公司内网的maven仓库,然后像这样去依赖:

implementation \'com.xxx.xxx:module_common:1.0.0\'

我们把三方库统一放到 config.gradle 内管理:

ext {
    dependencies = [
            \"glide\": \"com.github.bumptech.glide:glide:4.12.0\",
            \"glide-compiler\": \"com.github.bumptech.glide:compiler:4.12.0\",
            \"okhttp3\": \"com.squareup.okhttp3:okhttp:4.9.0\",
            \"retrofit\": \"com.squareup.retrofit2:retrofit:2.9.0\",
            \"retrofit-converter-gson\"  : \"com.squareup.retrofit2:converter-gson:2.9.0\",
            \"retrofit-adapter-rxjava2\" : \"com.squareup.retrofit2:adapter-rxjava2:2.9.0\",
            \"rxjava2\": \"io.reactivex.rxjava2:rxjava:2.2.21\",
            \"arouter\": \"com.alibaba:arouter-api:1.5.1\",
            \"arouter-compiler\": \"com.alibaba:arouter-compiler:1.5.1\",
            // our lib
            \"module_util\": \"com.sun.module:module_util:1.0.0\",
            \"module_common\": \"com.sun.module:module_common:1.0.0\",
            \"module_base\": \"com.sun.module:module_base:1.0.0\",
            \"fun_splash\": \"com.sun.fun:fun_splash:1.0.0\",
            \"fun_share\": \"com.sun.fun:fun_share:1.0.0\",
            \"export_biz_home\": \"com.sun.export:export_biz_home:1.0.0\",
            \"export_biz_me\": \"com.sun.export:export_biz_me:1.0.0\",
            \"export_biz_msg\": \"com.sun.export:export_biz_msg:1.0.0\",
            \"biz_home\": \"com.sun.biz:biz_home:1.0.0\",
            \"biz_me\": \"com.sun.biz:biz_me:1.0.0\",
            \"biz_msg\": \"com.sun.biz:biz_msg:1.0.0\"
    ]
}

这样方便版本统一管理, 然后在根目录的 build.gradle 内导入:

apply from: \'config.gradle\'

最后在各自的模块引入依赖,比如在 module_common 中这么引入依赖即可。

dependencies {
    api rootProject.ext.dependencies[\"arouter\"]
  kapt rootProject.ext.dependencies[\"arouter-compiler\"]
  api rootProject.ext.dependencies[\"glide\"]
  api rootProject.ext.dependencies[\"okhttp3\"]
  api rootProject.ext.dependencies[\"retrofit\"]
  api rootProject.ext.dependencies[\"retrofit-converter-gson\"]
  api rootProject.ext.dependencies[\"retrofit-adapter-rxjava2\"]
  api rootProject.ext.dependencies[\"rxjava2\"]
  api rootProject.ext.dependencies[\"module_util\"]
  api rootProject.ext.dependencies[\"module_base\"]
}

页面跳转

做完组件之间的隔离后,暴露出来最明显的问题就是页面跳转和数据通信的问题。一般来说,页面跳转都是显示startActivity跳转,在组件化项目内就不适用了,隐式跳转可以用,但每个Activity都要写 intent-filter 就显得有点麻烦,所以最好的方式还是用路由框架。

实际上市面已经有比较成熟的路由框架专门就是为了组件化而生的,比如美团的WMRouter,阿里的ARouter等,本例使用 ARouter 框架,看下ARouter页面跳转的基本操作。

首先肯定是引入依赖,以 module_common 引入ARouter举例,build.gradle 应该添加:

android {
    defaultConfig {
        javaCompileOptions {
       annotationProcessorOptions {
          arguments = [AROUTER_MODULE_NAME: project.getName()]
         }
    }
    }
    compileOptions {
      sourceCompatibility JavaVersion.VERSION_1_8
      targetCompatibility JavaVersion.VERSION_1_8
  }
}
dependencies {
    api rootProject.ext.dependencies[\"arouter\"]
  kapt rootProject.ext.dependencies[\"arouter-compiler\"]
}

kapt注解依赖没有办法传递,所以我们不可避免得需要在每个模块都声明这些配置,除了 api
rootProject.ext.dependencies[“arouter”] 这行。然后需要全局注册 ARouter,我是在 module_common 统一注册的。

class AppCommon: BaseApp{
    override fun onCreate(application: Application) {
        MLog.d(TAG, \"BaseApp AppCommon init\")
        initARouter(application)
    }

    private fun initARouter(application: Application) {
        if(BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(application)
    }
}

接着我们在 module_common 模块内声明一个路由表用作统一管理路径。

// RouterPath.kt
class RouterPath {
    companion object {
        const val APP_MAIN = \"/app/MainActivity\"
        const val HOME_FRAGMENT = \"/home/HomeFragment\"
        const val MSG_FRAGMENT = \"/msg/MsgFragment\"
        const val ME_FRAGMENT = \"/me/MeFragment\"
        const val MSG_PROVIDER = \"/msg/MsgProviderImpl\"
    }
}

然后在MainActivity类文件上进行注解:

@Route(path = RouterPath.APP_MAIN)
class MainActivity : AppCompatActivity() {
}

任意模块只需要调用 ARouter.getInstance().build(RouterPath.APP_MAIN).navigation() 即可实现跳转。如果我们要加上数据传递也很方便:

ARouter.getInstance().build(RouterPath.APP_MAIN)
            .withString(\"key\", \"value\")
            .withObject(\"key1\", obj)
            .navigation()

然后在MainActivity使用依赖注入接受数据:

class MainActivity : AppCompatActivity() {
    @Autowired
    String key = \"\"
}

Arouter方案

在 export_biz_msg 组件下声明 IMsgProvider,此接口必须实现 IProvider 接口:

interface IMsgProvider: IProvider {
    fun onCountFromHome(count: Int = 1)
}

然后在 biz_msg 组件里实现这个接口:

@Route(path = RouterPath.MSG_PROVIDER)
class MsgProviderImpl: IMsgProvider {
    override fun onCountFromHome(count: Int) {
          // 这里只是对数据进行分发,有监听计数的对象会收到
        MsgCount.instance.addCount(count)
    }
    override fun init(context: Context?) {
        // 对象被初始化时调用
    }
}

在 biz_home 首页组件中发送计数:

val provider = ARouter.getInstance().build(RouterPath.MSG_PROVIDER).navigation() as IMsgProvider
provider.onCountFromHome(count)

可以看到其实和页面跳转的方式基本雷同,包括获取 Fragment 实例的方式也是这种。ARouter把所有通信的方式都用一种api实现,让使用者上手非常容易。

Application生命周期分发

当 app 壳工程启动Application初始化时要通知到其他组件初始化一些功能。这里提供一个简单的方式。

首先我们在 module_common 公共库内声明一个接口 BaseApp:

interface BaseApp {
    fun onCreate(application: Application)
}

然后每个组件都要创建一个 App 类实现此接口,比如biz_home组件:

class HomeApp: BaseApp {
    override fun onCreate(application: Application) {
            // 初始化都放在这里
        MLog.d(TAG, \"BaseApp HomeApp init\")
    }
}

剩下最后一步就是从 app 壳工程分发 application 的生命周期了,这里用到反射技术:

val moduleInitArr = arrayOf(
    \"com.sun.module_common.AppCommon\",
    \"com.sun.biz_home.HomeApp\",
    \"com.sun.biz_msg.MsgApp\",
    \"com.sun.biz_me.MeApp\"
)
class App: Application() {
    override fun onCreate() {
        super.onCreate()
        initModuleApp(this)
    }
    private fun initModuleApp(application: Application) {
        try {
            for(appName in moduleInitArr) {
                val clazz = Class.forName(appName)
                val module = clazz.getConstructor().newInstance() as BaseApp
                module.onCreate(application)
            }
        }catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号