Android组件化框架实践

前言

之前考虑到后续可能会参与到小包项目的开发中,因此希望利用平时的时间形成2套框架,后续新项目立项后可立即投入使用,快速上线。本文说明的是一套基于jetpack+coroutines基础上形成的一套组件化框架,本文主要描述组件化项目的通用配置和关键技术点的解决方案。

为什么要组件化

大家知道,随着项目的业务逐渐发展,业务功能越来越多,参与的开发人员也可能变更,修改合并代码容易出现冲突,于是在出现了模块化的概念,即把同一个业务板块的内容单独抽取一个module去实现,做到代码隔离。但是模块化也会带来一些新的问题,不同的模块之间为了实现数据跳转,依赖关系错综复杂,代码耦合性很强。而且最致命的是,项目迭代到一定程度,由于项目本身比较大,全量编译耗时往往非常巨大,因此引入组件化。

总结下来组件化有如下2个优点:

  1. 加快编译速度。在开发阶段,业务模块(module_xxx)可以作为application独立编译,编译速度明显加快;
  2. 明确开发人员彼此职责。明确开发人员维护模块,只需要管理和熟悉相应的module,彼此互不打扰,公共基础功能根据是否是业务相关,降级到Common层或core层,或单独封装成lib调用;
  3. 更合规。相比插件化的解决方案,组件化并没有引入动态加载Apk的功能,保证在google play等商店上线时不会出现合规性问题;

项目架构图

架构图

以上架构图是目前我完成的项目中的模块依赖关系图,各个模块承载功能说明如下:

  1. App壳module中没有任何业务代码,只有全局Application对象的代码,和全局主题、logo等配置;

  2. Module_main、Module_sample、Module_A、Module_B为4个业务模块,后续可根据项目中真实的模块做修改;

  3. lib_download代表下载功能,是一个library,某个业务模块如需要直接依赖即可;

  4. lib_common作为项目业务模块的业务公共组件,包括所有和项目界面相关的组件或基类;

  5. lib_core代表所有与项目界面无关的组件和类库及资源,包括基类、图片加载、日志、网络请求、基础Util等

  6. lib_fileprovider代表fileprovider的公共定义处理,common库直接依赖即可;

  7. lib_webview代表webview页面的模块,由于业务模块基本都会跳转webview,所以直接common库去依赖;

组件化配置

  1. 在项目中的gradle.properties中添加组件化控制开关

    #当前是否是组件化模块,false表示整体编译,true表示分模块编译
    isComponentMode=false
    

    在开发阶段,将此参数调整为true,即可单个模块编译打包,合并打包测试或发布时,将此参数修改为false

  1. 新增模块调试入口启动类

    在module_XXX等业务模块下新建Debug包,并新增DebugActivity和XXXApplication作为组件化状态下,模块调试的入口类和Application对象,如下

    debug
  1. 新增模块编译时的Manifest文件

    在module_XXX等业务模块下新建module_manifest文件夹,提供AndroidManifest文件,此Manifest文件包含上面的DebugActivity文件作为App启动入口,及其他当前模块的所有Activity

    manifest
  2. 抽取module_main、module_A、module_B等业务模块的build.gradle文件中公共内容,形成一个模块通用的build.gradle文件,样例如下

if (Boolean.parseBoolean(isComponentMode)) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'


android {
    compileSdkVersion configs.android.compileSdkVersion
    buildToolsVersion configs.android.buildToolsVersion

    defaultConfig {
        minSdkVersion configs.android.minSdkVersion
        targetSdkVersion configs.android.targetSdkVersion
        versionCode configs.android.versionCode
        versionName configs.android.versionName

        multiDexEnabled true

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"

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

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            if (Boolean.parseBoolean(isComponentMode)) {
                //模块化,作为独立App应用运行
                manifest.srcFile 'src/main/module_manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                resources {
                    //合并打包版本时,排除debug文件夹下所有文件
                    exclude '*/debug/*'
                }
            }
        }
    }

    buildTypes {
        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    buildFeatures {
        viewBinding true
    }
}

注意点:

  • 此文件顶部的module类型配置,通过步骤一中的参数动态改变模块的module类型

  • sourceSets中通过步骤一的参数动态获取AndroidManifest文件

  1. 然后在对应的module_xxx中的build.gradle顶部添加此公共文件的依赖,样例如下:
apply from: "../module.build.gradle"

android {
    defaultConfig {
        if (Boolean.parseBoolean(isComponentMode)) {
            //Module作为独立App应用运行,需配置包名
            applicationId "com.zhl.module_main"
        }
    }
}

dependencies {
    implementation project(path: ':lib_common')
    //ARouter注解处理器
    kapt deps.arouter.arouter_compiler
}
  1. 壳工程依赖。设置在非组件化模式下,依赖各个业务模块合并编译
   dependencies {
       implementation fileTree(dir: "libs", include: ["*.jar"])
   
       if (!Boolean.parseBoolean(isComponentMode)) {
           //非组件化模式下,依赖各个模块
           implementation project(path: ':module_main')
           implementation project(path: ':module_sample')
           implementation project(path: ':module_a')
           implementation project(path: ':module_b')
       }
   
   }
  1. 页面跳转

组件化的第一目标就是业务模块间解耦,不相互依赖,如模块A的的一个Activity需要跳转模块B的一个Activity,在没有相互依赖的情况下,我们采用阿里的路由解决方案-ARouter,ARouter的配置过程参照官方文档,需要注意的点是kapt相关的依赖配置需要在各个业务模块module去添加,代码参考如下:

dependencies {
    implementation project(path: ':lib_common')
    //ARouter注解处理器
    kapt deps.arouter.arouter_compiler
}
defaultConfig {
        minSdkVersion configs.android.minSdkVersion
        targetSdkVersion configs.android.targetSdkVersion
        versionCode configs.android.versionCode
        versionName configs.android.versionName

        multiDexEnabled true

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"

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

然后在各个模块的Activity或Fragment上配置对应的path,则可实现跨模块跳转;

至此,可在修改isComponentMode参数不同的状态下实现分模块组件化编译运行或合并编译运行!

框架组件化技术点分析

通过上面的步骤相信你可以很快实现一个组件化框架的通用配置,但是如果你进一步深入组件化的使用后,会出现如下几个问题:

问题一:组件间数据传递

组件间的数据传递又分为两种情况:

1.页面主动跳转携带数据

组件化项目在业务开发过程中会出现如下问题:module_A和module_B之间要进行跳转,要使用一个共同的model,此时需要把该model对象放在Base中,但是随着业务的不断迭代,各个模块跳转之间跳转的业务会越来越多,导致model对象在base中越来越多,这部分内容严格来说是隶属于业务代码,因此在base模块和业务模块之间新增lib_common模块,统一管理Model实体类、图片资源、Strings等内容

2.模块数据被动拉取

例如:在module_main模块需要获取到module_user模块的用户信息去做显示。此场景使用ARouter的依赖注入进行处理

  1. 先在lib_common模块建立一个获取user信息的接口

    interface IUserProvider : IProvider {
    
        /**
         * 提供User模块的数据
         *
         * @param s
         */
        fun getUserData(): String
    }
    
  1. 然后在module_user模块实现

    @Route(path = ARouterConstant.USER.USER_DATA_PROVIDER)
    class UserProvider : IUserProvider {
    
        override fun getUserData(): String {
            return "这个是User模块的数据"
        }
    
        override fun init(context: Context?) {
    
        }
    }
    
  1. 再module_main模块获取数据

    val iSampleProvider = ARouter.getInstance()
                    .build(ARouterConstant.SAMPLE.SAMPLE_DATA_PROVIDER).navigation() as ISampleProvider
    Log.d("TAG值",iSampleProvider.getSampleData())
    

问题二: Application生命周期处理

Application作为程序启动的入口,其中的onCreate()方法经常做一些初始化相关工作,这些初始化的代码中,又包含2类,其中一类是所有模块都要用到的全局配置,如ARouter初始化、网络请求头配置、刷新样式、日志打印配置等。另外一类是某个模块特定的初始化配置。目前在组件化框架中有如下三种实现方式

方式一:统一处理

将全局配置代码统一全部放在Common层的Application的基类中,某个模块特定的初始化配置代码放在app壳module的Application配置

优点:处理简单,逻辑清晰

缺点:模块特定的初始化代码需要在其他module中调用,无法实现代码隔离

方式二: 反射处理
  1. 在lib_common模块中定义ICommonApplication

    interface ICommonApplication {
    
        fun onCreate()
    
    }
    
  1. 定义业务module配置类

    object ModuleApplicationConfig {
    
        private const val MODULE_MAIN = "com.zhl.module_main.MainApplication"
        private const val MODULE_SAMPLE = "com.zhl.module_sample.SampleApplication"
        private const val MODULE_A = "com.zhl.module_a.AApplication"
        private const val MODULE_B = "com.zhl.module_b.BApplication"
    
        val modules = mutableListOf(MODULE_MAIN, MODULE_SAMPLE, MODULE_A, MODULE_B)
        
    }
    
  1. 在每个业务模块中定义Application回调接收

    class MainApplication : ICommonApplication {
        
        override fun onCreate() {
            Log.d("Application生命周期", "MainApplication执行onCreate!!")
            //todo 执行Main_module模块需要特定执行的代码 
        }
        
    }
    
  1. 在CommonApplication类的OnCreate()方法中通过反射各个业务module的Application实例,然后执行相关初始化代码

    open class CommonApplication : BaseApplication() {
    
        override fun onCreate() {
            super.onCreate()
    
            initComponent()
        }
    
        private fun initComponent() {
            ModuleApplicationConfig.modules.forEach {
                val clazz = Class.forName(it)
                val instance = clazz.newInstance() as ICommonApplication
                instance.onCreate()
            }
        }
    }    
    

优点:可实现代码隔离,逻辑清晰

缺点:通过反射实现,有性能问题,新增模块是需要修改module配置类

方式三:APT处理

此方法引入一个新的AppLifeCycle插件,他可以无侵入的获取到Application的生命周期,具体使用步骤如下:

  1. lib_common模块依赖applifecycle-api依赖

    //AppLifecycle
    api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
    
  1. 各个业务模块module_xxx添加applifecycle-compiler注解处理器依赖(注意:java请用annotationProcessor关键字)

    kapt 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'
    
  1. 新建各个业务模块的Application,用于接受主Application的生命周期事件分发,以module_main模块的MainApplication为例:

    @AppLifecycle
    class MainApplication : IApplicationLifecycleCallbacks {
    
        /**
         * 设置优先级
         *
         * @return
         */
        override fun getPriority(): Int {
            return NORM_PRIORITY
        }
    
        override fun onCreate(context: Context?) {
            Log.d("Application生命周期", "MainApplication执行onCreate!!")
        }
    
        override fun onTerminate() {
    
        }
    
        override fun onLowMemory() {
    
        }
    
        override fun onTrimMemory(level: Int) {
    
        }
    
    
    }
    
  1. 壳工程添加AppLifecycle插件配置,并注册生命周期事件

    项目build.gradle修改如下:

    buildscript {
        ext.kotlin_version = "1.5.21"
        repositories {
            google()
            mavenCentral()
            //applifecycle插件仓也是jitpack
            maven { url 'https://jitpack.io' }
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:7.0.3'
            classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31'
    
            //加载插件applifecycle
            classpath 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-plugin:1.0.3'
        }
    }
    

    app模块build.gradle修改如下:

    //使用插件applifecycle
    apply plugin: 'com.hm.plugin.lifecycle'
    

    优点:实现代码隔离,通过APT插件模式处理,编译阶段完成引用,无性能问题

    缺点:代码逻辑调用无直接关联,要引入三方库

结论:目前项目经过评估引入方式三处理

其他

具体代码见:https://github.com/henryzhu-dev/AppArchitectureApplication

目前还在持续不断完善中,力求可以覆盖更多场景,实现最终快速实现业务,高质量交付的目的!

参考资料:

https://juejin.cn/post/6881116198889586701

https://juejin.cn/post/6844904147641171981

https://juejin.cn/post/6844904147641171981#heading-6

https://zhuanlan.zhihu.com/p/333887361

你可能感兴趣的:(Android组件化框架实践)