在进行app组件化之前我们要明白什么是组件化?为什么要组件化?
什么是组件化?为什么要组件化?
在项目的体系结构,代码量,功能,逻辑等不断的增长之后,项目的编译,开发的协作,效率等都会变得毫无体验。所以我们就要相处一个办法来解决这个事情。一般来说会有如下几种思路
1,组件化
2,插件化
3,模块化
如果对于app的动态化,体积等有比较大的考量则插件化是不错的选择。但是一般来说市面上除非像淘宝这种超级app,有这强大的技术底蕴的团队的app,其实大部分的app是不需要必须使用插件化的技术的。有些公司其实是完全跟风罢了。而且在安卓9.0之后插件化的提要其实并不友好,所以一般非必须的情况下并不台推荐使用插件化。
模块化我理解则是将组件化更加细粒度的进行拆分,比如可能会将一个输入栏作为一个模块进行复用,比如QQ的聊天输入,或者微信的聊天输入等类似的。这样的场景架构其实也是非一般公司可为。
当然你可以将三者组合起来使用,组件化的同时实现插件化,再将各个业务模块完全抽离,然后还有各种动态化,部分组件的跨平台等等。。
但是架构是需要一步一步的进化的,如果要一口气的整一个巨无霸出来,那只能将自己耗死,即使整出来,也一定会被自己憋死,因为吃的太饱了。
所以对于一个业务成长飞速,但体量又没有过于复杂的app来说,组件化是最好的架构升级方案,因为它足够的高效,升级之后的付出与收获成正比。
组件化要解决的问题
在进行组件化之前我们要思考为什么要组件化,组件化能为我们解决什么要的问题。
想象一个场景,我们在进行独立开发的时候,整个代码完全就是我们自己的世界,甚至我们都不用进行git pull 的操作,只需要git push即可,毫无代码冲突风险。
然而有一天小明来到了公司和你一起开发,这回你需要熟悉git的各种操作了,如何merge,俩人商量着如何不互相改代码。不过开发体验还是可以接受的。
又过了一段时间公司获得了马云100个亿的投资,老板飘了。老板决定每个开发的岗位增加二十个人。当然因为马爸爸投资了公司,公司自然不能差,业务飞涨,这二十个人自然也没有闲下来,每人都负责的一个小功能,比如小明同学在安心的做着商品详情相关的功能。大家每人维护着一个包,其乐融融。
但是你作为一个公司元老级别的老鸟很快发现了一个问题,而是多个兄弟在同时在一个包下工作,有一天你突然发现util包下有timeUtil,dateUtil等几个看起来很类似的工具类。并且你发现小明同学写代码的时候经常会不小心动了其他同学的代码,导致小明同学的人生安全产生了很大的隐患。
然后你发现一个更重要的问题,自己每次改一个小功能的时候,竟然都要花上一把王者荣耀的时间去等待app跑起来,偶尔还会编译失败。测试同学最近的脏话也月来越多。。。
公司代码混乱不堪,开发氛围压抑无比,看来解决问题迫在眉睫了!你发现了以下几点需要解决的问题:
1,公共组件的提取
2,每个人维护的模块内容,不冲突,不会互相伤害,独立调试
3,解决编译速度问题
4,模块跳转,通信等
如何组件化
先看一张图
组件化.png在整个结构的最上层就是app模块,其实他和module没有太大区别,本质上也是一个module,只不过是一个壳module,使用他来做一些集成其他子module的操作与分发。
而在结构的第二层便是各种module,比如order模块,login模块等等,每个模块项目隔离,相互独立。
在结构的第三层是base,router,bus共同模块,按需集成。这里我没有把brouter和bus放到base中。
在结构的最下层则是我们公司各种通用的基础设施,比如http模块,可能有多个项目使用,可能是第三方库(okhttp),或者公司基础设施的团队自己维护的libray,一般情况都是通过aar来引入。当然这里为了直观,我把这些以源码的形式做到工程中。
这里有俩个东西是必备的,router和bus
Router
市面上有很router库,功能大同小异,这里推荐阿里的Arouter
https://github.com/alibaba/ARouter
用法不多介绍,很简单
说下使用router的必要性和好处
1,模块之间的activity是不能通过原生去跳转的,这是router首要解决的问题。
2,router没有intent传参大小限制的问题。
3,router有强大的拦截器和降级
4,router可以是模块之间通信的重要手段,如服务的发现等。
5,对组件化的埋点和统计等有着很好的帮助。
。。。
Bus
bus主要解决的是组件之间通信的问题,替代广播等原生方案,bus有很多中,eventbus,rxbus, livedataBus等。。
这里我简单的写了个livedatabus来作为组件化的bus方案。livedatabus的好处是生命周期的感知,重要的是简单。避免了eventbus的各种注解,迷之传递。
实践
首先砍下我写的demo结构图
2019-05-22 11-24-46屏幕截图.png
这里我简单的写了一些gradle脚本来配置工程
2019-05-22 11-26-46屏幕截图.png
一,module的动态引用
app的gradle文件
apply plugin: 'com.android.application'
apply from: "$rootProject.projectDir/buildScript/main_build.gradle"
android {
defaultConfig {
versionCode rootProject.versionCode
versionName rootProject.versionName
}
}
main_build.gradle文件
在这个文件中,我们读取一个gradle的参数来识别壳工程需要引用的module,然后动态的引用,这样就避免了我们为了独立的调试需要不停的修改module的类型。然后在编译期间把个参数插入到string中方便我们在代码中使用。
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/buildScript/buildTypes/appBuildTypes.gradle"
apply from: "$rootProject.projectDir/buildScript/buildTypes/appProductFlavors.gradle"
apply from: "$rootProject.projectDir/buildScript/router/arouter.gradle"
apply from: "$rootProject.projectDir/buildScript/module_common_build.gradle"
def getDependencyModule() {
return project.properties.get("dependencyModule")
}
def modules = getDependencyModule().split(',')
android{
buildTypes {
release {
resValue "string", "modules", getDependencyModule()
}
debug {
resValue "string", "modules", getDependencyModule()
}
}
}
modules.each {
module ->
print(module)
project.dependencies.add("implementation", project(':' + module))
}
然后我们在gradle的配置文件中配置一个参数来引用相应的module
每次我们需要修改引用的module时就改一下这里的配置即可。
#需要依赖的moudle
dependencyModule=moudle1,moudle3
二,module的applaction初始化
我们每个module可能除了一些公有的库之外,会引入一些只有自己会使用的库,我们无法在app的applaction中作初始化的操作,所以我们必须要为每个module都作初始化。
这里我们在router模块中协理一个applaoction初始化的服务。
interface IAppInit : IProvider {
fun initApp(applaction: Application)
}
然后需要初始化的module去实现这个接口,并把路由的地址暴露出来。
比如这里是module1的初始化实现。
@Route(path = AppModules.module1AppInit)
class Module1Applaction : IAppInit {
override fun initApp(applaction: Application) {
Log.e("Module1Applaction","initApp")
}
override fun init(context: Context) {
}
}
然后我们AppModules这个里面配置我们的路由地址。
object AppModules {
const val module1AppInit = "/module1/appInit"
const val module2AppInit = "/module2/appInit"
const val module3AppInit = "/module3/appInit"
fun getModulePath(module: String): String? {
return when (module) {
"moudle1" -> module1AppInit
"moudle2" -> module2AppInit
"moudle3" -> module3AppInit
else -> null
}
}
}
接着我们就可以在我们的app的主applaiction中去根据我们配置的modules去获取每个module相应的初始化实现进行初始化,这样就实现了每个module都可以使用自己的初始化方案了。这是我们要解决的第二个问题。
private fun initModules() {
val modules = getString(R.string.modules).split(",")
modules.forEach { module ->
val modulePath = AppModules.getModulePath(module)
if (modulePath != null) {
val navigation = ARouter.getInstance().build(modulePath).navigation()
if (navigation != null && navigation is IAppInit) {
navigation.initApp(this)
}
}
}
}
三,模块的独立调试
同样我们为每个模块引入下面的脚本
apply from: "$rootProject.projectDir/buildScript/module_build.gradle"
android {
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
versionCode rootProject.versionCode
versionName rootProject.versionName
}
}
module_build.gradle脚本
在这个脚本中,我们同样根据gradle的配置参数来动态的配置
如果配置的参数中有我们的module,那么就为modlue引入:apply plugin: 'com.android.library'使其成为一个libray。反之则引入 apply plugin: 'com.android.application' 时期成为一个可以独立调试的app。当然这样还是不能够进行调试的,我们还需要动态的配置AndroidManifest的路径。
def getDependencyModule() {
return project.properties.get("dependencyModule")
}
def modules = getDependencyModule().split(',')
def contains = modules.contains(project.getName())
if (contains) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
delete project.buildDir
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/buildScript/router/arouter.gradle"
apply from: "$rootProject.projectDir/buildScript/buildTypes/appBuildTypes.gradle"
apply from: "$rootProject.projectDir/buildScript/module_common_build.gradle"
android {
sourceSets {
main {
if (contains) {
manifest.srcFile 'src/main/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
}
}
}
}
2019-05-22 12-16-37屏幕截图.png
如上图,我们在main的同级别配置一个debug,在module没有被引入的时候我们就使用这里面的AndroidManifest,我们在AndroidManifest中配置我们的applaction,以及启动的activity。之后我们就可以直接进行调试启动了。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xiaofeiluo.moudle1">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:name=".debug.Module1Applaction"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<activity android:name=".Module1HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
如下图便是没有将module引入app中的时候
2019-05-22 12-21-27屏幕截图.png
四,组件之间通信的问题
组件和组件之间是相互独立的,想要进行通信只能通过下层的共同模块来进行。一共有俩种方案
方案一:面向接口的通信,既是服务暴露
这种方案一般来说是我们一个模块想要获取另一个模块的数据,或者说一个模块做了一个服务需要share给其他的模块我们可以使用这中方案。
比如下面的例子
我们需要获取module3中的用户数据,那么我们需要在router中定义一个接口
interface IUserInfoService : IProvider {
fun getUserName(callback: (name: String) -> Unit)
fun getUserAge(callback: (age: String) -> Unit)
fun getUserSchool(callback: (school: String) -> Unit)
}
然后我们要在module3中去实现这个接口,并且把路由地址配置到这个实现类
@Route(path = Module3RouterPath.UserInfoService)
class UserInfoService : IUserInfoService {
private var name: String? = null
private var age: String? = null
private var school: String? = null
private var handler: Handler? = null
override fun getUserName(callback: (name: String) -> Unit) {
if (TextUtils.isEmpty(name)) {
thread {
Thread.sleep(1000);
name = "张三"
handler?.post {
callback.invoke(name!!)
}
}
} else {
callback.invoke(name!!)
}
}
override fun getUserAge(callback: (age: String) -> Unit) {
if (TextUtils.isEmpty(age)) {
thread {
Thread.sleep(1000);
age = "10"
handler?.post {
callback.invoke(age!!)
}
}
} else {
callback.invoke(age!!)
}
}
override fun getUserSchool(callback: (school: String) -> Unit) {
if (TextUtils.isEmpty(school)) {
thread {
Thread.sleep(1000);
school = "清华大学"
handler?.post {
callback.invoke(school!!)
}
}
} else {
callback.invoke(school!!)
}
}
override fun init(context: Context?) {
handler = Handler(Looper.getMainLooper())
}
}
然后在module1中我们就可以获取这个服务去使用了
getName.setOnClickListener {
val userInfo = ARouter.getInstance().build(Module3RouterPath.UserInfoService).navigation()
userInfo?.let {
if (it is IUserInfoService) {
it.getUserName {
name.text = it
}
}
}
}
方案二,bus
bus的方案我们一般用来主动的去监听一些变化的发生,而不想接口是去获取,bus是主要解决接受的通信。
这里我简单写了个livedatabus来作为总线,然后我们面向具体的数据模型进行监听。
这里我用一个叫做event的注解标识这是一个可以被传递的event,注解中的内容是这个evnet对应的key,
bus的原理就不多作介绍。
@Event("UserEvent")
class UserEvent(var newName: String)
然后我们就可以去监听变化了
接受消息
BusManager.call(UserEvent::class).observe(this) {
name.text = it.newName
}
发送消息
updateName.setOnClickListener {
BusManager.postEvent(UserEvent("李四"))
}
好了,到这里我们就完成了组件之间通信的方案。
最后说几个问题。
1,为什么bus,router,base要分开维护?
这里我觉得base作为一个功能是每个模块都需要的,一般来说是由一个伙计去维护的,这个东西一般来说也是以一种aar的形式去集成的。而route和bus则不同,他们是需要每个模块的小伙伴自己去维护的,所以物品们要在router和bus之内去明确的分包,所以改动可能会很频繁,我们避免不小心碰触到base中的核心内容,最好把这些配置类的东西分离出来独立维护。还有一种办法是,这俩个东西我们统一由一个小伙伴维护,其他的模块想要在里面添加服务,或者路由,那么就需要统一的告诉这个小伙伴并且配备相应的说明文档,这个小伙伴再进行统一的审核。
2,关于工程的一些配置
这里我么可以利用gradle把一些没必要重复的配置抽离出来,同是也可以统一的进行管理,比如一些每个模块都需要的libray,junit,apt,kpt这些的可以拿出去。然后关于版本的话我们可以每个模块进行独立的维护,因为我们各个模块的versionCode不一定一样,大部分情况下,我们是需要CI进行配合来自动化构建的,所以模块一般也是需要发aar来引用。模块之间也有独立的仓库,一般我们会用submodule的形式引入自己开发的模块,别人的模块我们则需要使用aar的形式来引用。所以可以根据实际的需求来写一些gradle的脚本来扩展我们的工程。比如说我们加个配置来决定使用源码引用还是aar引用等。
这个只是最简单的一系列配置,解决了基本的组件化方案的思路,实际情况可能要复杂很多,但是基本的问题已经解决。
网友评论