在APP开发的初期,代码了不大,业务量比较下,一个APP作为一个单独的模块进行开发,往往问题不大,而且还能加快开发效率但是随着APP的用户量越来越多,也越来越复杂,这种开发方式显得结构特别的臃肿,特别是多个开发人员进行开发维护一个项目的时候每个人的代码质量也不相同,容易会产生代码冲突的问题。同时随着业务的增多,代码变的越来越复杂,每个模块之间的代码耦合变得越来越严重,导致代码混乱,没法进行单元测试,bug修改往往会牵一发而动全身,改掉一个bug,有产生新的bug,同时编译时间也会越来越长,这时候解耦问题急需解决。APP模块化的目标是告别结构臃肿,让各个业务变得相对独立,业务模块在组件模式下可以独立开发,而在集成模式下又可以变为依赖包集成到“app壳工程”中,组成一个完整功能的APP。
一、模块化与组件化区别
1、模块化
- 使用:按照项目功能需求划分成不同类型的业务框架(例如:首页,视频,个人中心,支付……)。
- 目的:隔离/封装。
- 依赖:模块之间有依赖的关系,可通过路由器进行模块之间的耦合问题。
- 特点:高内聚、低耦合。
- 架构定位:横向分块(位于架构业务框架层)。
2、组件化
- 使用:各种自定义的控件、工具类等重复应用的代码等等。
- 目的:复用,解耦。
- 依赖:组件之间低依赖,比较独立。
- 特点:高重用、低耦合。
- 架构定位:纵向分层(位于架构底层,被其他层所依赖)。
因此,在模块化开发的同时引入组件化将会使的工程更加的架构更加清晰。
二、组件化的整体架构
- app:项目的宿主模块,仅仅是一个空壳,依赖于其他模块,成为项目架构的入口。
- baselibrary:项目的基类库,每个子模块都依赖共享公用的类和资源,防止公用的功能在不同的模块中有多个实现方式。
- module_begin:闪屏页,引导页,主页等。
- module_home:首页模块。
- module_mine:我的模块。
- module_video:视频模块。
从上面的例子可以看出各个模块的划分是根据业务划分,这样的好处显而易见,当项目业务很庞大时候,在维护项目过程中只需要更改某一模块,而不涉及其他的模块,这样更加解耦,更加安全,另外各个模块可单独编译打包某一模块,提升开发效率。
三、模块的独立运行
在项目开发中,各个模块可以同时开发,独立运行而不必依赖于宿主APP,也就是每个module是一个独立的app,项目发布的时候依赖到宿主APP中。各业务模块之间不允许存在相互依赖关系,但是需要依赖基类库。单一模块生成的apk体积也小,编译时间也快,开发效率会高很多,同时也可以独立测试。要实现这样的效果需要对项目做一些配置。
1、gradle.properties配置
在项目gradle.properties中需要设置一个开关,用来控制module的编译,如下:
2、清单文件配置
module清单文件需要配置两个,一个作为独立项目的清单文件,一个作为库的清单文件,以module_begin模块为例:
buildApp作为依赖库的清单文件,和独立项目的清单文件buildModule区别是依赖库的清单文件Application中没有配置入口的Activity,其他都一样,如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.arouter.module_begin"> <application android:allowBackup="true" android:supportsRtl="true" android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <activity android:name="com.arouter.module_begin.MainActivity"/> <activity android:name="com.arouter.module_begin.SplashActivity"/> </application> </manifest>
3、gradle配置
gradle文件中加入:
if (isModule.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } android { defaultConfig { if (isModule.toBoolean()) { applicationId "com.arouter.module_match" } sourceSets { main { if (isModule.toBoolean()) { manifest.srcFile 'src/main/buildModule/AndroidManifest.xml' } else { manifest.srcFile 'src/main/buildApp/AndroidManifest.xml' } } } } } dependencies { implementation project(':baselibrary') }
4、宿主app配置
dependencies { if (!isModule.toBoolean()) { implementation project(':module_home') implementation project(':module_video') implementation project(':module_begin') implementation project(':module_mine') } }
经过这四步配置之后就可以通过isModule进行配置,当isModule为true的时候作为单独的模块进行运行,当isModule为false作为依赖库,从而进行灵活的开发。
四、模块之间的通信之ARouter
模块与模块之间不存在依赖关系,而是各自运作的,页面的跳转就不能直接startActivity调用具体的activity了,因为这个Activity已在另外一个模块,中那么如何进行页面跳转,管理页面?这里要谈到页面路由的概念,简单的来说映射页面跳转关系的,当然它也包含跳转相关的一切功能。而是ARouter开源框架恰好满足这个需求,他是是阿里巴巴开源的Android平台中对页面、服务提供路由功能的中间件,提供跨模块API调用,通过控制反转来做组件解耦,提供参数传递、解析,跳转拦截等强大的功能,目前start数量已经接近7K。
1、集成
(1)在app和baselibrary的gradle中分别引入:
android { defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } } } } annotationProcessor 'com.alibaba:arouter-compiler:1.1.4' compile 'com.alibaba:arouter-api:1.3.1'
(2)在所有的module的gradle中加入:
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4' android { defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } } } }
arouter-api的作用是引入arouter,只需要在app和baselibrary中引入,而arouter-compiler是通过arouter依赖注入生成一些额外的的代码,必须在每个gradle中引入,否则没法生成依赖的代码,导致跳转失败;javaCompileOptions也是每个必须的,否则编译失败。
2、注册
集成之后还需要ARouter进行注册,建议在Application中进行注册,如下:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initRouter(this);
}
private void initRouter(MyApplication myApplication) {
if (BuildConfig.DEBUG) {
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(myApplication);
}
@Override
public void onTerminate() {
super.onTerminate();
ARouter.getInstance().destroy();
}
}
别忘记在清单文件中进行设置application,除此之外还需要在使用ARouter的地方再次进行注入,否则跳转不成功:
ARouter.getInstance().inject(this);
一般在基类Activity或者Fragment进行设置,让其他子类继承即可:
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.getInstance().inject(this);
}
}
3、模块间页面跳转
(1)最简单的页面跳转
ARouter.getInstance().build("/begin/MainActivity").navigation();
@Route("/begin/MainActivity")
public class MainActivity extends BaseActivity {
}
这样就可以跳转到MainActivity,需要注意的是"/begin/MainActivity"至少需要两级目录,不同模块之间第一个目录应该是不同的,一般以模块名称作为一级目录,Activity名称作为二级目录,Fragment类似。
(2)携带参数的页面跳转
ARouter.getInstance().build("/begin/MainActivity")
.withString("name", "张三")
.withInt("age", 30)
.navigation();
@Route(path = "/begin/MainActivity")
public class ClickButtonActivity extends BaseActivity {
@Autowired(name = "name")
public String name;
@Autowired(name = "age")
public int age;
}
}
通过@Autowired注解可以获取到传递过来的数据,注解后面的name可以省略掉,如果省略掉会以下面成员变量作为key进行获取,为了保险期间尽量设置name,否则key与成员变量不一致会导致失败,获取的数据为null。
(3)携带监听的页面跳转
ARouter.getInstance().build("/begin/MainActivity")
.withString("name", "张三")
.withInt("age", 30)
.navigation(getActivity(), new NavigationCallback() {
@Override
public void onFound(Postcard postcard) {
Log.e("TAG", "onFound:找到了");
}
@Override
public void onLost(Postcard postcard) {
Log.e("TAG", "onLost:没找到");
}
@Override
public void onArrival(Postcard postcard) {
Log.e("TAG", "onArrival:完成了");
}
@Override
public void onInterrupt(Postcard postcard) {
Log.e("TAG", "onInterrupt:被拦截了");
}
});
@Route(path = "/begin/MainActivity")
public class ClickButtonActivity extends BaseActivity {
@Autowired(name = "name")
public String name;
@Autowired(name = "age")
public int age;
}
}
NavigationCallback就是跳转的监听的回调,跳转成功、失败、完成、被拦截四个监听回调,这在个回调里面可以进行埋点设置、其他跳转、验证登录等等。
(4)跳转的事件拦截
ARouter提供了全局跳转的拦截功能,这个拦截功能用于跳转的前期做一些其他事情,例如验证登录等,可以定义多个拦截器,只需要加上@Interceptor注解就可以,不用在其他进行进行调用,注解上可以携带参数,例如@Interceptor(priority = 4),priority表示优先级,数字越小越会优先拦截,此外还有个name属性表示拦截器名称,基本上不怎么使用。
@Interceptor(priority = 4)
public class TestInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
//进行逻辑判断
if (true) {
callback.onContinue(postcard);
Log.e("TestInterceptor","正常");
} else {
// callback.onInterrupt(null);
callback.onInterrupt(new RuntimeException("异常"));
Log.e("TestInterceptor","数据异常进行拦截");
}
}
@Override
public void init(Context context) {
}
}
但是需要注意的是,每次所有的跳转都会执行拦截器操作,arouter提供了greenChannel()方法进行跳转过去一切拦截器,在不需要拦截器的地方跳转的时候加上即可。
ARouter.getInstance().build(JumpUtil.MainActivity).greenChannel().navigation();
4、模块间服务调用
用于模块之间进行服务的调用,可以获取其他模块的方法和数据。
(1)在公共模块baselibrary中注册接口一个接口继承自IProvider,加上所需要的方法。
public interface IUserInfo extends IProvider {
String getName();
}
(2)在提供服务的模块中实现这个接口,并且加上@Route注解,设置路径。
@Route(path = "/service/UserService")
public class UserInfoImpl implements IUserInfo {
@Override
public String getName() {
return "张三";
}
@Override
public void init(Context context) {
}
}
(3)在需要获取服务的地方进行数据获取
String name = ARouter.getInstance().navigation(IUserInfo.class).getName();
这样就可以调用其他模块的方法,获取到其他模块服务提供的数据了。
五、模块间的数据传递
(1)广播
在一个模块中发送广播设置数据,在另一个模块中注册广播接收数据,使用广播进行数据传递方式广播相对于其他的方式而言消耗资源较多。
(2)EventBus
调度灵活,不依赖于 Context,快速且轻量,在这里不做介绍。
六、模块化的注意事项
1、编译失败
(1)没有在每个build.gradle中加入:
javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } }
(2)依赖资源冲突
改为所以依赖的包版本一致,统一起来。
(3)没有注册
在Application下进行注册ARouter。
2、跳转失败
(1)在基类或者onCreate中没有注入。
ARouter.getInstance().inject(this);
(2)分组错误
不同模块的moudle在ARouter是不同的组,如果没有设置分组,默认使用一级目录分组,在设置一级目录的时候不同模块需要设置不同名称,一般以module名称命名一级目录名称。
(3)没有添加arouter-compiler依赖
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
到此结束!!!
demo运行效果如下: