随着业务的积累,产品的迭代,咱们写的工程会愈来愈大,也愈来愈臃肿,更加难以维护,那有没有一种方法,可以使得每一个人专门负责本身的业务模块,使用的时候把每一个人作的模块直接拼装组合起来就行,这样代码也更加灵活,相互之间的耦合性也更低,重用性也可以更大。那么模块化的概念就来了。html
简单来讲, 模块化就是将一个程序按照其功能作拆分,分红相互独立的模块,以便于每一个模块只包含与其功能相关的内容。模块咱们相对熟悉,好比登陆功能能够是一个模块, 搜索功能能够是一个模块, 汽车的发送机也但是一个模块。java
固然从我的的理解上,模块化只是一种思想,就是大化小,分开治理,在实际项目中如何具体实施,目前有两种方案,一个是组件化,一个是插件化
在网上找到了一张很形象的图android
组件化方案就是:由若干独立的子模块,组合成一个总体,下降模块间的耦合,这些子模块在补足必定的条件下,均可独立运行。主模块也不会由于缺乏任意子模块而没法运行。组件之间能够灵活的组建。git
相似于积木,拼装组合,易维护github
插件化方案就是:一个程序的辅助或者扩展功能模块,对程序来讲插件无关紧要,但它能给予程序必定的额外功能。web
打个比方,就像如今的应用程序,更多的须要依赖一些第三方库,好比地图sdk、分享sdK、支付sdk等等,致使安装包变得愈来愈大,单是依赖这些sdk,安装包可能就会额外的增长10-20M的大小;api
当须要新增功能时,不得不从新更新整个安装包。再熟读一下上面的定义,就知道它的用途和做用了,那就是有些附加功能,须要时,可灵活的添加,动态的加载。插件化主要是解决的是减小应用程序大小、免安装扩展功能,当须要使用到相应的功能时再去加载相应的模块
安全
区别根据他们使用的用途,就很好理解了:组件化在运行时不具有动态添加或修改组件的功能,可是插件化是能够的
markdown
提及组件化的实践方案,只有一首小诗形容, 走遍了各类论坛,看遍了地老天荒,原来最适合的方案啊,就在身旁app
总而言之一句话:各类方案都有,也不缺少不少写的不错的,可是秉持着商用开发为主,接下来介绍一个最合适的,那就是阿里巴巴出的一套ARouter,它简单易用、它支持多模块项目、它定制性较强、它支持拦截逻辑等诸多优势,接下来会写阿里这套框架的使用方便往后开发。若是有兴趣的小伙伴,能够等我下一篇博客,介绍它的实践原理。
就是一个电商,有3个组件,一个是首页,一个是购物车,一个是我的中心,3个独立的模块
由于每个模块都是要可以单独调试的,因此咱们先定义每一个模块的开关,设置这个模块是否要进行单独调试运行
buildscript { ext.kotlin_version = '1.3.31' ext { isRunHome = true // true是Home模块可单独运行 isRunPersonalcenter = true isRunShopingcar = true } 复制代码
if(isRunHome.toBoolean()){ // 1.根据以前设定的isRunHome,判断是否须要独立运行 apply plugin: 'com.android.application' }else { apply plugin: 'com.android.library' } android { android { compileSdkVersion 29 buildToolsVersion "29.0.0" defaultConfig { if(isRunHome.toBoolean()){ // 2.这里也设置一下,可运行的话,添加applicationId applicationId "com.bj.home" } 复制代码
dependencies { if(!isRunHome.toBoolean()){ // 1.若是要独立运行,那么主工程不加载它 implementation project(path: ':home') } implementation project(path: ':personalcenter') implementation project(path: ':shoppingcar') 复制代码
编译一下就是这样
4.固然还差一步,设置AndroidManifest.xml文件,由于通常来讲,一个APP只有一个启动页,在组件单独调试时也须要一个启动页,因此咱们须要设置两个文件。就这样
AndroidManifest文件和ApplicationId 同样都是能够在 build.gradle 文件中进行配置的,因此咱们一样经过动态配置组件工程类型时定义的 boolean变量的值来动态修改。须要咱们修改子模块(如home)的build.gradle文件。
android { ... sourceSets { main { // 1.单独调试与集成调试时使用不一样的 AndroidManifest.xml 文件 // 咱们还能够根据不一样工程配置不一样的 Java 源代码、不一样的 resource 资源文件等的 if(isRunHome.toBoolean()) { manifest.srcFile 'src/main/manifest/AndroidManifest.xml' } else{ manifest.srcFile 'src/main/AndroidManifest.xml' } } } } 复制代码
大功告成,使用时只须要修改根目录build.gradle文件中的那3个变量,就能够一键开启该模块的单独运行模式了,亲测有效,好了,咱们已经完成了,模块独立化了,子模块可单独运行了,可是,怎么通信,传递数据呀?组件与组件之间都是不能够直接使用类的相互引用来进行数据传递的!
解决办法就是集成集成阿里的路由框架ARouter,一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通讯、解耦 来咱们集成一下
1.在各个模块中添加了对 ARouter 的依赖,固然本身新建一个base模块,依赖添加到base里,其余模块引用它也能够。
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
复制代码
dependencies { compile 'com.alibaba:arouter-api:1.2.1.1' annotationProcessor 'com.alibaba:arouter-compiler:1.1.2.1' ... } 复制代码
好了,配置完成
咱们在自定义的MyApplication中,初始化它
@Override public void onCreate() { super.onCreate(); // 这两行必须写在init以前,不然这些配置在init过程当中将无效 if(isDebug()) { // 打印日志 ARouter.openLog(); // 开启调试模式(若是在InstantRun模式下运行,必须开启调试模式!线上版本须要关闭,不然有安全风险) ARouter.openDebug(); } // 初始化ARouter ARouter.init(this); } private boolean isDebug() { return BuildConfig.DEBUG; } 复制代码
1.在目标Activity添加注解 Route (home : HomeAty)
/** * 首页模块 * * 其中 path 是跳转的路径,这里的路径须要注意的是至少须要有两级,/xx/xx * */ @Route(path = "/home/HomeAty") public class HomeAty extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_home); } } 复制代码
2 页面跳转(app : MainActivity)
@Override public void onClick(View view) { switch (view.getId()){ // 跳转Activity页面 case R.id.btn_go_home: ARouter.getInstance().build("/home/HomeAty").navigation(); break; } } 复制代码
@Override public void onClick(View view) { switch (view.getId()){ ... // 跳转Activity页面, 而且返回数据 case R.id.btn_go_aty_forresult: ARouter.getInstance().build("/home/HomeResultAty").navigation(this, 897); break; } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == 897 && resultCode == 999){ String msg = data.getStringExtra("msg"); tv_msg.setText(msg); } } 复制代码
@Route(path = "/home/HomeResultAty") public class HomeResultAty extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_home_result); findViewById(R.id.btn_goback).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent in = new Intent(); in.putExtra("msg", "从home模块返回的数据"); setResult(999, in); finish(); } }); } } 复制代码
Fragment mFragment = (Fragment) ARouter.getInstance().build("/home/HomeFragment").navigation(); getSupportFragmentManager().beginTransaction().replace(R.id.fl, mFragment).commit(); 复制代码
2.固然fragment也要加注解(home : HomeFrag)
@Route(path = "/home/HomeFragment") public class HomeFrag extends Fragment {...} 复制代码
// 携参数跳转 case R.id.btn_go_home_byArgs: ARouter.getInstance().build("/home/arg") .withString("msg", "5") .withDouble("msg2", 6.0) .navigation(); break; 复制代码
2.目标Activity(home: HomeByArgAty)
@Route(path = "/home/arg") public class HomeByArgAty extends Activity { @Autowired(name = "msg") String arg1; @Autowired String arg2; private TextView tv_msg; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_home_arg); // 若是使用Autowired注解,须要加入底下的代码 // 固然也能够用 getIntent().getStringExtra("") ARouter.getInstance().inject(this); tv_msg = findViewById(R.id.tv_msg); tv_msg.setText("从主工程传递过来的参数:"+arg1); } } 复制代码
---------------------------------进阶用法------------------------------
ARouter也添加了拦截器模式,拦截器有不少用处,好比路由到目标页面时,检查用户是否登陆,检查用户权限是否知足,若是不知足,则路由到相应的登陆界面或者相应的路由界面。ARouter的拦截器比较奇葩,只须要实现IInterceptor接口,并使用@Interceptor注解便可,并不须要注册就能使用。固然这也有了它的坏处,就是每一次路由以后,都会通过拦截器进行拦截,显然这样程序的运行效率就会下降。Interceptor能够定义多个,好比定义登陆检查拦截器,权限检查拦截器等等,拦截器的优先级使用priority定义,优先级越大,越先执行。拦截器内部使用callback.onContiune()/callback.onInterrupt(),前者表示拦截器任务完成,继续路由;后者表示终止路由。例子:
1.实现IInterceptor接口,自定义拦截器,检测全部跳转中,只要uri为空就拦截,也能够在这请求中再加内容
@Interceptor(priority = 4) public class LoginInterceptor implements IInterceptor { @Override public void process(Postcard postcard, InterceptorCallback callback) { String uri = postcard.getExtras().getString("uri"); if(TextUtils.isEmpty(uri)){ Log.i("lybj", "uri为空,中断路由"); callback.onInterrupt(null); }else { Log.i("lybj", "拦截器执行,uri不为空,继续执行吧"); postcard.withString("msg", "能够随意加内容"); callback.onContinue(postcard); } } @Override public void init(Context context) { } } 复制代码
// 拦截器测试 case R.id.btn_test_interceptor: ARouter.getInstance().build("/home/web") .withString("uri", "file:///android_asset/schame-test.html") .navigation(); break; 复制代码
3.目标界面
@Route(path = "/home/web") public class WebAty extends Activity { private WebView wv_web; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_web); Log.i("lybj", getIntent().getStringExtra("msg")); String uri = getIntent().getStringExtra("uri"); wv_web = findViewById(R.id.wv_web); wv_web.loadUrl(uri); } } 复制代码
这个很坑,翻了翻文档,说是要在所引用的全部的model的build.gradle里面都要加上下面的代码
defaultConfig{
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
复制代码
或者
defaultConfig{
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
复制代码
AROUTER_MODULE_NAME和moduleName根据不一样的版本,选择不一样的名字,上面的代码要保证全部模块都要添加,为的是作区分
做者就作了个蠢事,两个model的layout名字同样,主工程加载的时候,老是出问题,因此尽量的保证每一个model的资源名加前缀。