在项目开发过程中,随着业务与人员的增加,如果没有提前使用合理的架构,代码会变得越来越臃肿,功能耦合性也越来越高。为了代码的质量,这时候我们需要对工程进行重构。
比较简单的重构方案就是代码按照模块划分,也就是Android中的module概率,每一个module对应一个模块,各自的代码在各自的module中提交,这种情况下是合理的,但是不同的模块有可能涉及相同的功能。
比如一个模块有购买功能,而另一个模块也有购买功能,这时候无论把购买功能的代码放到哪个模块都不能避免模块间的耦合,因此为了解决这种问题就有了组件化的思想。
一、组件化与模块化的对比
模块化:模块化对应的是独立业务模块。
组件化:组件化是单一的功能组件,每一个都可以使用module开发并可以提供sdk发布使用。
对比而言模块化是业务为导向,而组件化是功能为导向。
模块可以包含多个组件,因此模块化的颗粒度明显大于组件化。
两者的目的都是一致的都是为了工程的解耦。
组件化基础架构图:
上面是最简单的组件化架构图
base是基类,集成常用的基础框架
login/pay是组件module,依赖base,提供各种的功能需求。
最上层的app是模块或者主工程,依赖这三个组件完成业务的需求。
二、组件化面临的问题
组件化开发过程中需要面临的几个问题:
-
1.工程中每个组件都需要独立运行的能力也需要被集成到主工程运行,这样也能够提升组件的编译与运行速度,节省开发时间。
-
2.组件间的数据传递与方法的调用,这是我们需要解决的。因为组件之间可能需要状态判定,比如购买需要登录的状态,那么如何优雅的获取组件的数据是一个问题。
-
3.如何在不强依赖的情况下优雅的完成组件之间界面的跳转。
-
4.如何在不强依赖的情况下优雅的完成组件Fragment的在主工程的创建
-
5.主工程集成调试时,如何在不依赖工程的情况下主工程也不报错。
-
6.如何实现代码隔离,资源隔离
三、组件实现独立调试与集成调试
Android Studio使用Gradle构建工程,Gradle提供了三种插件,通过配置不同的插件构建不同的工程。
- App 插件,id: com.android.application
- Library 插件,id: com.android.library
- Test 插件,id: com.android.test
base是纯粹的library,因此只需要引用library即可
apply plugin: 'com.android.library'
llogin/pay组件则需要做些额外的配置,因为它们需要支持集成到app项目中,也需要能够支持独立运行。
在集成到主工程中时,它们是library。独立运行时,它们是Application。因此就需要配置一个字段来设置该工程是否是library还是Application。
3.1动态配置插件与applicationId
在module中新增gradle.properties文件,文件中配置
isRunAlone = false //true代表独立运行,false代表是以library形式运行。
在module的build.gradle文件中,依赖插件语句针对新增字段做判断:
if (isRunAlone.toBoolean()) {
//独立运行
apply plugin: 'com.android.application'
} else {
//依赖主工程运行
apply plugin: 'com.android.library'
}
同时,作为library不需要applicationId,所以需要在配置中增加对新增字段的处理:
defaultConfig {
if (isRunAlone.toBoolean()) {
applicationId "com.xj.paymodule"
}
...
}
3.2动态配置manifest文件
module在独立运行时有自己的主入口文件,在集成到主工程时如果不处理,主工程合并manifest后会导致多个入口出现,这是个问题。
因此我们需要额外一份manifest文件,然后在gradle文件中动态配置manifest调用。
如图:
主工程依赖:
独立运行:
在gradle中动态配置manifest.srcFile属性
android{
...
sourceSets {
main{
if (isRunAlone.toBoolean()){
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
...
}
此时组件化的第一步已经完成了。这时候通过更改新增属性的值就可以动态的配置module独立运行或者依赖主工程运行。
四、组件间数据传递与方法的相互调用
这时候我们需要考虑另一个问题,组件之间通信问题。比如pay组件在购买时需要判断登录状态,那么就需要获取login组件的状态。当然我们可以通过直接依赖的形式来处理,但是这就违背了组件相互独立的原则,所以需要我们解决这个问题。这里我们采用了接口的方式来解决这个问题。
4.1新增componsentbase工程
新增名为componsentbase的module工程,定义Service接口。创建Factory类,为所有Service提供调用方法。
工程图如下:
public interface IAccountService {
boolean isLogin();
String getAccountId();
}
public class ServiceFactory {
private IAccountService mAccountService;
private ServiceFactory(){
}
public static ServiceFactory getInstance(){
return Inner.serviceFactory;
}
private static class Inner {
private static ServiceFactory serviceFactory = new ServiceFactory();
}
public void setAccountService(IAccountService service){
mAccountService = service;
}
public IAccountService getAccountService(){
if (null == mAccountService){
mAccountService = new EmptyAccountService();
}
return mAccountService;
}
}
4.2组件负责接口的实现,并完成设值
LoginModule中提供接口的实现,并在Application中完成注册:
//实现
public class LoginService implements IAccountService {
@Override
public boolean isLogin() {
return null != AccountUtils.getInstance().getAccountInfo();
}
@Override
public String getAccountId() {
return null == AccountUtils.getInstance().getAccountInfo() ? "" :
AccountUtils.getInstance().getAccountInfo().accountId;
}
}
//注册
public class LoginApp extends Application {
@Override
public void onCreate() {
ServiceFactory.getInstance().setAccountService(new LoginService());
}
4.3组件之间调用
pay组件调用:
if (ServiceFactory.getInstance().getAccountService().isLogin()){
Toast.makeText(PayActivity.this,"购买成功",Toast.LENGTH_LONG).show();
} else {
Toast.makeText(PayActivity.this,"请先登录",Toast.LENGTH_LONG).show();
}
通过这样的方法完成组件间交互,避免组件之间相互依赖。这时候我们发现一个问题,LoginApp组件只有在独立运行的时候才会调用,集成运行时,组件的Application是主工程的Application,因此我们需要处理一下集成运行时各组件Application配置注册的问题。
我们采用反射的技术来完成各组件Application动态配置的问题。
在componsentbase工程中创建BaseApp类,该类是抽象类继承Application。
4.4组件Application数据初始化
public abstract class BaseApp extends Application {
public abstract void initModuleConfig(Application application);
public abstract void initModileData(Application application);
}
提供了初始化的方法。
主工程Application和组件Application都继承BaseApp,并实现其抽象方法。接口的注册等相关调用都移入到实现方法中。
Login组件:
public class LoginApp extends BaseApp {
private static final String TAG = "LoginApp";
@Override
public void onCreate() {
super.onCreate();
//独立运行会调用
initModuleConfig(this);
}
@Override
public void initModuleConfig(Application application) {
ServiceFactory.getInstance().setAccountService(new LoginService());
}
@Override
public void initModileData(Application application) {
}
}
主工程:
主工程中配置需要反射调用的类名。然后在Application中反射调用。
public class MainApp extends BaseApp {
private static final String TAG = "MainApp";
//组件的application名称
private static final String LoginApp = "com.xj.loginmodule.LoginApp";
//配置需要反射application数组
public static String[] moudleApps = {LoginApp};
@Override
public void onCreate() {
super.onCreate();
//调用
initModuleConfig(this);
initModileData(this);
}
@Override
public void initModuleConfig(Application application) {
//反射调用initModuleConfig方法
for (String appName : moudleApps){
try {
Class> appClass = Class.forName(appName);
BaseApp baseApp = (BaseApp) appClass.newInstance();
baseApp.initModuleConfig(application);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
@Override
public void initModileData(Application application) {
//反射调用initModileData方法
for (String appName : moudleApps){
try {
Class> appClass = Class.forName(appName);
BaseApp baseApp = (BaseApp) appClass.newInstance();
baseApp.initModileData(application);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
}
运行日志:
=====MainApp onCreate =====
=====MainApp initModuleConfig =====
=====LoginApp initModuleConfig =====
=====MainApp initModileData =====
=====LoginApp initModileData =====
这样不通过强依赖的就完成了依赖组件的配置。当然也有缺点就是需要手动配置依赖的Application名称。
到现在,组件化的搭建就已经基本完成了。我们可以通过主工程运行一下:
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,LoginActivity.class);
startActivity(intent);
}
});
findViewById(R.id.pay).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, PayActivity.class);
startActivity(intent);
}
});
正常!
五、组件间之间优雅的调用
测试代码是通过显示意图来实现界面间的跳转,显然不符合我们解耦的需要。毕竟组件支持可配置,如果当前主工程不依赖组件,那么工程就会编译报错这是不能容忍的。当前我们可以使用隐式意图,但是隐示式图需要通过manifest文件管理,协作比较麻烦,所以我们采用更灵活的方式,使用开源的 ARouter 来实现。
一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
路由是指从一个接口获取数据包,从数据包中获取路由包的目的路径并进行定向转发到另一个接口的过程。因此可以用来组件化解耦。
要进行ARoute跳转就需要进行组件对ARoute依赖,组件依赖于Base组件,所以我们需要在Base组件中依赖ARoute。
5.1ARoute组件引入
ARoute依赖:
Base工程:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
...
}
dependencies {
...
api 'com.alibaba:arouter-api:1.5.0'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
其余任何依赖需要使用ARouter的module:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
...
}
dependencies {
...
//需要 不然生成不了路由表
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
5.2ARoute组件初始化:
主工程Application中:
public class MainApp extends BaseApp {
private static final String TAG = "MainApp";
@Override
public void onCreate() {
super.onCreate();
...
if (isDebug()) { // 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this);
...
}
}
5.3ARoute组件路由注册
路由注册:
通过注解Route,path中/xx/xx为路径 主要路径至少两级。
@Route(path = "/login/login")
public class LoginActivity extends AppCompatActivity {}
@Route(path = "/pay/pay")
public class PayActivity extends AppCompatActivity {}
5.4组件跳转:
通过path跳转
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ARouter.getInstance().build("/login/login").navigation();
}
});
findViewById(R.id.pay).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ARouter.getInstance().build("/pay/pay").navigation();
}
});
这样就完成主工程与组件之间的解耦。
5.5路由过滤拦截功能
pay模块调用时要依赖login状态,登录成功时,则进行pay,否则中断调用。
下面针对此业务编写简单的拦截器:
// 比较经典的应用就是在跳转过程中处理登陆事件,这样就不需要在目标页重复做登陆检查
// 拦截器会在跳转之间执行,多个拦截器会按优先级顺序依次执行
@Interceptor(priority = 8,name = "登录拦截器")
public class LoginInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
//callback.onContinue(postcard); // 处理完成,交还控制权
// callback.onInterrupt(new RuntimeException("我觉得有点异常")); // 觉得有问题,中断路由流程
// 以上两种至少需要调用其中一种,否则不会继续路由
if (TextUtils.equals(postcard.getPath(),"/pay/pay")){
if (ServiceFactory.getInstance().getAccountService().isLogin()){
callback.onContinue(postcard); // 处理完成,交还控制权
} else {
callback.onInterrupt(new RuntimeException("请先登录"));// 觉得有问题,中断路由流程
}
} else {
callback.onContinue(postcard); // 处理完成,交还控制权
}
}
@Override
public void init(Context context) {
// 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次
}
}
拦截器需要实现IInterceptor,重写process方法。priority是优先级,name是拦截器描述。拦截器在跳转之间调用。触发process方法后,callback.onContinue和 callback.onInterrupt必须要调用一个,不然不会继续路由。上面代码判断是否是pay跳转,是的话则判断是否登录,登录则继续路由,否则中断路由。
六、组件之间优雅的获取Fragment
除了Activity,Android中也有需要Fragment的获取。通常情况我们会直接new Fragment来引用。但是现在为了主工程与组件之间的解耦,直接new的方式就不适用了。因此我们需要额外的方式去实现。ARouter新版本支持Fragment路由,因此我们可以直接使用ARouter做处理:
Fragment类添加@Route注解
@Route(path = "/login/loginFragment")
public class LoginFragment extends Fragment {}
通过路由获取Fragment实例
Fragment fragment = (Fragment) ARouter.getInstance().build("/login/loginFragment").navigation();
获取到实例后续操作和以前一样。
这样就完成了Fragment与主工程的解耦。除了借助ARouter组件外我们还可以通过接口的方式来解耦。
原先接口中新增方法
public interface IAccountService {
...
void newFragment(Context context, int containId, FragmentManager manager, Bundle bundle,String tag);
}
实现类中处理附载Fragment的逻辑
public class LoginService implements IAccountService {
...
@Override
public void newFragment(Context context, int containId, FragmentManager manager, Bundle bundle, String tag) {
FragmentTransaction fragmentTransaction = manager.beginTransaction();
LoginFragment fragment = new LoginFragment();
fragment.setArguments(bundle);
fragmentTransaction.add(containId,fragment,tag);
fragmentTransaction.commitNow();
}
}
主工程调用:
ServiceFactory.getInstance().getAccountService().newFragment(this,R.id.content,getSupportFragmentManager(),null,"");
这样也完成了解耦并且保证了主工程编译不会失败。
七、主工程集成调试
到现在工程解耦基本上完成了,集成调试的问题在上面几个问题中已经解决了。通过componsentBase组件的各个Service接口解决了直接引用类的问题,并且保证主工程能够访问依赖的工程。通过ARouter组件进行组件之间的跳转与获取Fragment。这样组件之间没有强依赖,所以即便主工程不依赖组件,主工程也不会编译失败。
七、代码隔离与资源隔离
我们面向接口编程方式来完成工程解耦的,但是主工程还是能访问到组件的代码。那么就有可能有意无意的直接引用到组件的类,这样一来上面的解耦就白做了。所以我们想要主工程只有在打包阶段才能访问组件的代码,在开发阶段不能访问,这样就杜绝了直接引用的存在。
这个问题我们通过gradle去实现,gradle3.0提供了新的依赖方式runtimeOnly,同过runtimeOnly依赖的组件只有在运行时对工程和消费者可用,开发阶段完全隔离。
所以我们需要在主工程依赖时采用runtimeOnly方式:
dependencies {
...
runtimeOnly project(path: ':payModule')
runtimeOnly project(path: ':loginModule')
runtimeOnly project(path: ':livemodule')
}
这样主工程就不会引用到组件的代码了。
在gradle版本中这样虽然避免的了代码引用,但是资源确还是能够引用的。新的版本上主工程引用组件的资源会报红,因此新版本基本上已经达到资源隔离了。那么我们看下老版本gradle的做法:
android {
...
resourcePrefix "login_"
...
}
通过在组件的gradle中配置resourcePrefix字段来添加资源前缀。当资源不以前缀开头的话则资源会报红,而以前缀开头则正常。
但是resourcePrefix只能限制xml中的资源并不能限制图片资源,因此我们针对图片资源需要手动设置前缀,同时将共用的资源放入到Base库中,这样最大化的实现资源的隔离。
到这里组件化基本上结束了。最终结构变成了如下所示:
本文是针对组件化学习的记录,主要参考以下文章,感谢作者:
组件化最佳实践