项目模块化/组件的个人理解
随着项目越来越大,编译的时间会越来越长,参与开发测试的人也会越来越多,各个功能模块之间也会越来越耦合难以复用,模块化和组件化就是为了提高开发测试的效率,降低功能模块之间的耦合性,使得它们可以复用,那么什么是模块化?什么是组件化?他们有什么区别呢?
- 组件:指的是具有单一功能的控件,比如扫一扫控件、图片选择控件、城市选择、日期选择、分享、自定义组件等
- 模块:指的是与业务相关的功能、比如订单模块、搜索模块、商品模块等等,一个业务模块中可能用到多个组件
所以模块化就是将一个项目根据实际业务需求,划分成多个业务模块,不同模块之间是要能够明确划清界线互不影响,使得各个模块能够像独立的app一样单独编译、运行、开发、测试,也能够复用到多个app中;
而组件化就是要把那些具有单一功能的控件独立出来,使得它们能够像第三方开源项目那样,可以运用于多个模块或者多个app项目中;
最后将不同业务模块根据需要组装成app,这就好像组装一辆汽车一样,不同的零件可以由不同的公司生产,零件之间也是相对独立的,如果某个零件坏了,只需要更换那个零件,而不会影响到其他零件!
项目模块化/组件化优点
通过对项目模块化/组件化之后,就可以:
- 多个团队并行开发测试,各自维护各自所在模块/组件
- 修改某个模块/组件后,只需要测试当前模块/组件,而不需要对整个项目回归测试
- 开发过程中只需要编译运行所在的模块/组件,不需要对整个项目编译,提高了开发效率
- 独立出来的模块/组件可以复用到多个app项目中
模块化/组件化遵循原则
在进行模块化/组件化过程中要遵循以下原则:
- 只有上层模块可以依赖下层模块,下层模块不能依赖上层模块
- 同一层的模块/组件之间不能相互依赖以保证他们之间彻底解耦
模块化/组件化架构设计
通常可以分成三层架构:
- 应用层:主项目,也就是一个app壳,用于将各个模块组装起来
- 组件层:各个业务模块、或者独立功能组件
- 基础层:公共基础组件,与业务完全无关的,比如:图片加载、接口请求、自定义组件等
不管是模块、app、还是组件,在Android Studio中都是以Module形式存在的,Module主要有三种类型:
- apply plugin: ‘com.android.application’,编译生成apk文件,可以直接运行
- apply plugin: ‘com.android.library’,编译生成aar文件,可以包含资源文件
- apply plugin: ‘java’,编译生成jar包
模块化/组件化需要解决的问题
-
开发时需要将Module改成application,使得开发当前模块时可以单独编译运行,在打最终包的时候又需要改成library供app主项目使用,如何通过配置gradle进行动态配置?
1、 在整个项目的gradle.properties中添加一个自定义变量isDebug,如果为true,表示项目处于各个模块开发阶段,false表示各模块开发结束,需要集成到单个app中//gradle.properties isDebug = true
2、 然后在各个Module下的build.gradle中通过isDebug做判断:
如果为true则将Module类型改为application,如果为false则Module改为Library类型
如果需要使用不同AndroidManifest.xml则在Module的build.gradle中配置sourceSets{main{if(isDebug.toString()){manifest.srcFile ‘src/main/…’}else {}}
由于Library不需要applicationId,所以也需要判断,if(isDebug.toBoolean()){applicationId “xxx.”}if(isDebug.toBoolean()){ apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } android{ defaultConfig { if(isDebug.toBoolean()){ applicationId "com.example.home" } } sourceSets{ main { if(isDebug.toBoolean()){ manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } }
3、 接着在主项目app中的build.gradle根据isDebug判断,如果是true,则不引入各个Module,只有为false,需要集成进来时才引入
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) if(!isDebug.toBoolean()){ implementation project(':home') implementation project(':login') } implementation project(':core') }
-
由于各个Module中的资源最终都要合并到主项目,所以需要注意文件命名不能重复,解决的办法就是每个Module的build.gradle文件中配置资源文件的前缀,但是有个缺点,图片资源如果不加前缀不会有任何提示:
android{ resourcePrefix = "core_" }
或者在根项目下的build.gradle统一配置:
subprojects { afterEvaluate { android { resourcePrefix "${project.name}_" } } }
-
每个Module都可能需要依赖第三方库,以及配置compileSdkVersion、buildToolsVersion、minSdkVersion版本号,为了统一管理这些版本号,可以在根项目的build.gradle中定义:
ext { versionCode = 1 versionName = "1.0" compileSdkVersion = 28 buildToolsVersion = "29.0.2" minSdkVersion = 15 targetSdkVersion = 28 lib_constraintlayout = "androidx.constraintlayout:constraintlayout:1.1.3" lib_appcompat = "androidx.appcompat:appcompat:1.1.0" lib_core_ktx = "androidx.core:core-ktx:1.0.2" lib_test_runner = "androidx.test:runner:1.1.1" lib_test_espresso = "androidx.test.espresso:espresso-core:3.1.1" lib_junit = "junit:junit:4.12" lib_rxjava = 'io.reactivex.rxjava2:rxjava:2.2.3' lib_rxandroid = 'io.reactivex.rxjava2:rxandroid:2.1.0' lib_rxbinding = 'com.jakewharton.rxbinding2:rxbinding:2.0.0' lib_arouter_api = 'com.alibaba:arouter-api:1.3.1' lib_arouter_compiler = 'com.alibaba:arouter-compiler:1.2.0' }
然后最底层的基础Module中统一引用:
android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { inSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName } } dependencies { api fileTree(dir: 'libs', include: ['*.jar']) api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" api rootProject.ext.lib_appcompat api rootProject.ext.lib_core_ktx api rootProject.ext.lib_appcompat api rootProject.ext.lib_constraintlayout testImplementation rootProject.ext.lib_junit androidTestImplementation rootProject.ext.lib_test_runner androidTestImplementation rootProject.ext.lib_test_espresso api rootProject.ext.lib_rxjava api rootProject.ext.lib_rxandroid api rootProject.ext.lib_rxbinding api rootProject.ext.lib_arouter_api kapt rootProject.ext.lib_arouter_compiler }
-
组件Application初始化问题:由于组件最后是编译成aar供主项目依赖,所以并不会调用其他组件的Application,但是我们通常会需要在Application中初始化一些第三方库,解决的思路是:
1、在基础组件里添加一个抽象Application基类abstract class BaseApplication : Application() { override fun onCreate() { super.onCreate() init(this) Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler()) } abstract fun init(application: Application) }
2、然后在其他组件中船创建Application,并实现基类的init方法
class HomeApplication : BaseApplication() { override fun onCreate() { super.onCreate() init(this) } override fun init(application: Application) { AppLogger.log(this.javaClass, "初始化HomeApplication成功") ARouter.init(application) } }
3、基础组件中添加统一调用各个组件Application的init方法
class ApplicationConfig { companion object { val APPLICATION_LIST = arrayOf( "com.example.home.HomeApplication", "com.example.login.LoginApplication" ) fun init(application: Application) { for (applicationClassString: String in APPLICATION_LIST) { try { val applicationClass = Class.forName(applicationClassString) val applicationInstance = applicationClass.newInstance() if (applicationInstance is BaseApplication) { applicationInstance.init(application) } } catch (e: Throwable) { e.printStackTrace() } } } } }
4、最后在主项目的Application中调用ApplicationConfig的init方法
class AppApplication : BaseApplication() { override fun init(application: Application) { ApplicationConfig.init(this) } }
-
组件之间通信跳转问题:由于不同模块之间为了彻底降低耦合性,不能直接互相引用,所以需要解决不同Module之间跳转Activity,以及传递数据,调用其他module方法,消息通知等
-
目前Activity之间跳转主流方案是使用路由的方式,比如阿里的ARouter,它的实现原理是通过自定义注解Route等,使用APT在编译阶段解析注解并生成相应的类文件用于保存路径和对应的类,然后在Application启动时调用ARouter.init方法,获取编译阶段生成的那些类,将路径和对应的类保存起来,然后在需要跳转时调用ARouter.getInstance().build()方法传入路径,然后通过map找到对应的类,with方法传入要传递的数据,最后调用navigation方法调用startActivity进行跳转;
-
当然我们也可以自己简单实现不同模块之间的跳转,原理就是使用反射要获取要跳转的类,在使用的时候传入要跳转Activity完整的路径就可以的,在基础的Module中增加工具类:
class ActivityUtil { companion object { fun open(context: Context, activityClassName: String) { open(context, activityClassName, Intent()) } fun open(context: Context, activityClassName: String, intent: Intent) { try { val clazz = Class.forName(activityClassName) intent.setClass(context, clazz) if (context !is Activity) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(intent) } catch (e: Throwable) { Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() } } } }
-
当我们需要获取另一个Module中的Fragment或者调用某个功能的时候,可以通过注册服务接口/获取服务接口来调用其他Module的功能,当然也可以直接使用ARouter:
-
首先在基础的Module中添加相应服务的接口:
/** * Login模块服务 * * @author zhujie * @date 2019-09-12 * @time 14:59 */ interface ILoginService { /** * 是否已经登录 */ fun isLogin(): Boolean /** * 获取登录界面的Fragment */ fun getLoginFragment():Fragment } ```
-
接着在对应的Module中创建对应的服务类,并实现该接口
class LoginService : ILoginService { private val mIsLogin = false override fun isLogin(): Boolean { return mIsLogin } override fun getLoginFragment(): Fragment { return LoginFragment() } }
-
接着需要在基础组件中添加用于管理所有服务的类
/** * 服务管理类 * * @author zhujie * @date 2019-09-12 * @time 15:08 */ class ServiceManager { var loginService :ILoginService ?=null//登录模块的服务 fun getInstance():ServiceManager{ return InstanceHolder.instance } companion object{ private class InstanceHolder{ companion object{ val instance = ServiceManager() } } } }
-
然后在对应Module的Application类中注册该服务
class LoginApplication : BaseApplication() { override fun onCreate() { super.onCreate() init(this) } override fun init(application: Application) { //注册登录模块提供的服务 ServiceManager.getInstance().loginService = LoginService() } }
- 最后在其他Module中使用:
class HomeActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.home_activity_main) if(ServiceManager.getInstance().loginService.isLogin()){ mHomeLoginButton.text = "已登录" } else {//未登录 addDisposable(RxView.clicks(mHomeLoginButton) .subscribe { ARouter.getInstance().build(RouterPath.LOGIN_LOGIN_ACTIVITY).navigation() }) } } }
-
-
当我们登录或者退出登录之后一般会有很多地方需要对登录操作做相应处理,这些需要处理的地方可能分布在不同模块中,这个时候就可以使用EventBus,通过事件总线发送消息,达到不同模块解耦目的
-