(本文提出的组件化项目已经开源,参见YouJu。*注:请勿商用,如有违反,责任自负)
前言(废话)
最近有段空闲期,公司的这个项目一直由我负责,之前一直为了效率为忽略了质量,加上之前项目的功能的不断叠加,所以现在项目体积变得非常庞大且冗杂。但是考虑到日后可能其他人接手,而且自己有点完美主义和强迫症,就想利用这段时间对项目进行重构。重构的目的无非是使项目功能及模块具体划分开来,避免过于耦合,牵一发而动全身,这样后续的功能开发和维护也会更加方便和快速。具体功能模块单独抽离出来,不会影响其他的模块。这样每个人只需要负责自己的那部分工作就可以了。自然而然想到了组件化。其实关于组件化的文章已经到处都是了。但是始终都是别人的思想转化出来的文字。自己可能看一篇就过了,不会有多么深入的理解和记忆。于是就想单独写一套并且搭建一套日后可以通用的开发框架。于是就有了这个项目,到现在项目基本已经成型了,并且目前也已经实现了很多功能和内容。
概述
YouJu是一款集多个平台的资讯内容、复合型优秀资源和实用功能以及多种炫酷UI特效的组件化Demo,打破传统限制,融合多元,发现更多、更优秀、更有趣的东西。 项目是一款组件化综合案例,包含新闻,MONO资讯,开眼视频,知乎日报,垃圾分类搜索,一言,古诗词,智能聊天机器人,语音识别功能,干货集中营,豆瓣影视等等模块。项目利用业余时间开发,时间不固定,暂时只实现了这些功能。后续也会加入更多新的功能。参照该项目少许配置即可适用于任何通用项目开发框架,简单易上手,傻瓜式操作。
当前模块有:base基础模块、network网络请求模块、语音识别模块、工具模块、干货模块、资讯模块。除base和network外,每个模块都可单独抽离为APP运行,不影响主工程,具体参照config.gradle配置文件
项目采用组件化架构模式以及MVP+Walle+ARouter+AndroidX+Dagger2+Retrofit2+Okhttp3+ RxJava2+EventBus+ Kotlin/Java混编模式开发。
整个项目框架结构
流程
分析
此外,base还提供了ServiceProvider和Interceptor供上层调用,ServiceProvider可以提供一个服务给外部调用,比如A模块中的AServiceProvider依赖BaseServiceProvider,BaseServiceProvider是base中定义的基类提供器。A模块可以使用AServiceProvider提供AFragment或者其他A模块中的内容或者方法给B模块使用,只需要AServiceProvider配置一个路径,然后B模块访问这个路径,ARouter会去寻找这个路径,如果找到B模块就可以得到AServiceProvider实例服务,然后执行相应的业务逻辑。找不到也不会崩溃。这样就实现了跨模块服务调用。
public interface BaseServiceProvider extends IProvider {
BaseFragment newInstance(Object... args);
void customFun(Bundle bundle);
}
@Route(path = ConfigConstants.PATH_MODULE_PROVIDER)
public class AServiceProvider implements BaseServiceProvider {
@Override
public void init(Context context) {
}
@Override
public BaseFragment newInstance(Object... args) {
return AFragment.getInstance();
}
@Override
public void customFun(Bundle bundle) {
......
}
}
Interceptor是作为一个拦截器,它作用于整个ARouter页面跳转,也可以配置某个页面跳转不通过拦截器。取决于具体业务场景。常规的就是登录跳转拦截,A→B,如果已经登录直接到B,如果没有登录,先去C登录,登录成功后直接到B,B如果返回直接到A。
@Interceptor(priority = 5, name = "login")
public class LoginInterceptor implements IInterceptor {
private Context mContext;
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
Bundle extras = postcard.getExtras();
boolean isNeedIntercept = false;
if (extras != null) {
isNeedIntercept = extras.getBoolean(ConfigConstants.IS_NEED_INTERCEPT, false);
}
if (isNeedIntercept) {//需要执行登录拦截逻辑
boolean isNeedLogin = new Random().nextInt(10) % 2 == 0;
if (isNeedLogin) { //需要登录
//主线程跳转登录页面(走绿色通道,不走拦截器)
Single.just(postcard)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Postcard>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onSuccess(Postcard postcard) {
Bundle bundle = null;
if (null != postcard) {
String targetPath = postcard.getPath();
bundle = postcard.getExtras();
if (null != bundle && !TextUtils.isEmpty(targetPath) && !TextUtils.equals(ConfigConstants.PATH_LOGIN, targetPath)) {
bundle.putString(ConfigConstants.PATH_TARGET, targetPath);
}
}
ARouter.getInstance().build(ConfigConstants.PATH_LOGIN)
.with(bundle)
.greenChannel()
.navigation();
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
});
callback.onInterrupt(null);
} else {//不需要登录
callback.onContinue(postcard);
}
} else {//不需要拦截
callback.onContinue(postcard);
}
}
@Override
public void init(Context context) {
// 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次
mContext = context;
}
}
有时候我们还需要在Application中初始化一些东西,但是各个模块又是可以单独抽离的,所以不能将所有的初始化操作都放在一个Application中,不然还是会耦合,这里呢可以采用在base模块中定义一个BaseApplication,用于初始化base模块中需要初始化的操作,然后其他中间层模块如果需要初始化,就可以通过MouleApplication继承BaseApplication,然后执行所需要的初始化。这种情况可以适用于中间层模块作为单独APP运行时,但是当中间层模块作为library提供给壳时,这个时候整个应用只有一个Application,也就是壳的Application,所以中间层模块的MouleApplication这个时候不会被执行。这个时候就需要另外一种方式去实现。可以在base模块中定义一个BaseApplicationImpl接口,然后需要初始化的中间模块可以实现BaseApplicationImpl,然后在BaseApplication中分别遍历执行所有BaseApplicationImpl的实现类。剩下的就是把每一个实现类的具体路径加入到初始化路径列表ModuleConfig.MODULESLIST中即可。
public interface BaseApplicationImpl {
void onCreate(BaseApplication application);
}
private void modulesApplicationInit() {
for (String moduleImpl : ModuleConfig.MODULESLIST) {
try {
Class<?> clazz = Class.forName(moduleImpl);
Object obj = clazz.newInstance();
if (obj instanceof BaseApplicationImpl) {
((BaseApplicationImpl) obj).onCreate(this);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
public class ModuleConfig {
public static final String[] MODULESLIST = {
"com.heyongrui.iflytek.mscv5plusdemo.IflytedApplicationImpl"
};
}
public class IflytedApplicationImpl implements BaseApplicationImpl {
@Override
public void onCreate(BaseApplication application) {
StringBuffer param = new StringBuffer();
param.append("appid=" + application.getString(R.string.app_id));
param.append(",");
param.append(SpeechConstant.ENGINE_MODE + "=" + SpeechConstant.MODE_MSC);
SpeechUtility.createUtility(application, param.toString());
}
}
如果A模块的某个页面需要跳转到B模块的某个页面,但是App壳只依赖了A模块,这个时候就会跳转失败,而且还会弹出Toast,就会很烦,对于用户体验是不好的;亦或是A模块的某个页面的一个View需要根据App壳是否依赖B模块动态设置显示隐藏,这个时候可以通过checkIsHasPath方法去判断,这样就可以兼容到两种情况而且不会弹出Toast提示。
private boolean checkIsHasPath(String path) {
try {
String group = path.substring(1, path.indexOf("/", 1));
LogisticsCenter.completion(new Postcard(path, group));
} catch (Exception e) {
return false;
}
return true;
}
感悟
以上就是该项目的大致情况通过这个项目,中间也遇到了不少的问题,做起来其实没有说起来那么容易,体会到了抽丝剥茧的艰难。找到一个合适的方案固然重要,更重要的是克服重重困难坚定的实施下去。在整个过程中,不断去完善才有了现在的样子。总的来说,它学习成本比较低,可以快速的入手,最主要的是代码结构清晰了很多。如果你也面临着庞大的工程或者在重构的边缘犹豫不决,建议你赶紧行动起来,不然后面你会更难行走。也欢迎使用该方案,以最小的代价尽快开始实施组件化。如果你现在负责的是一个开发初期的项目,代码量还不大,那么更建议尽快进行组件化的规划,不要给未来的自己增加徒劳的工作量。
github源码已上传,喜欢的给个star,欢迎小伙伴fork~