Android组件化方案实践与思考
Demo地址:https://github.com/751496032/ComponentDemo
效果图:
效果图
背景
Android从诞生到现在,不知不觉的走过十多个年头了,也产生了很多App,随着项目的推进不断的迭代,而App也从最初的单一功能演变成多任务功能,各种业务的错综复杂,开发人员也不断的增加,如果架构不做调整优化,会给开发带来很大的困难:
- 各种业务代码耦合性及高,代码臃肿会越来越高,不利于团队间协同开发,维护成本也高;
- 降低开发效率,工程的编译运行时间及长,在单一工程下,每修改一小处都要运行整个项目,导致非常耗时。
- ……
基于代码耦合性问题,App的设计架构也不断的演变,从最初的MVC,到现在主流的MVP、MVVM,这些模式也确实起到代码解耦的效果,但还是很大局限性的;各业务间的耦合、运行效率问题都还是存在的;于是组件化思想就诞生了,组件化有如下优势:
- 业务间代码互不干扰,解耦性好,代码复用性也高;
- 各个组件能单独生成apk,可以单独调试,降低了编译运行时长。
组件化思想
组件化就是把单一工程的app分成多个Module,每个Module就相当于一个组件,而这些组件是不需要相互依赖的,可根据开发需求,自由将各个组件进行Application
、Library
模式切换进行调试开发。
上面是组件化基础结构图,从上向下分为三部分,分别是app空壳、功能组件(业务组件)、基础组件;
- App空壳只有一个组件就是App组件,需要依赖于各个业务组件,最终上线的就是App统筹所有的业务组件打包生成的;
- 功能组件又称之业务组件,各个组件间并没有依赖关系,除了Login组件外,把Login组件单独分开的原因是,我认为基本上所有的业务组件都需要登录行为才可以操作,不排除少数业务组件是不需要的,于是干脆把所有的业务组件全部依赖于Login组件,在这里Login组件其实是一个共享组件;
- 基础组件,这个很好理解就是我们封装的基础库,比如网络、路由、推送、图片等等
组件化需解决的问题
- 模式切换,如何使每个Module在
Application
、Library
间自由切换; - 依赖关系,如何处理每个Module间、工具类库的依赖关系,这个没有唯一模型,可根据项目需求也定,但一点可以肯定的是,同一层次的组件模块不能存在相互依赖的关系,不然就失去了组件化的意义了;
- 资源冲突,如何处理App空壳中所依赖的Module间资源重名冲突;
- 组件通信,如何处理业务组件间通信问题。
- ……
上面这几个问题是组件化实现过程中的主要问题,解决了上述问题,组件化方案实施基本没有多大的问题,其他的一些问题可根据自身需求而定。
实现步骤
1、在项目根目录下的gradle.properties
配置全局参数,方便管理各个Module的常用全局参数,比如版本号、常量等等
isModuleRun=false
compile_sdk_version=26
min_sdk_version=17
target_sdk_version=26
version_code=1
version_name=1.0
constraint_layout_version=1.1.3
support_version=26.1.0
leakcanary_version=1.6.1
arouter_version=1.3.1
arouter_annotation_version=1.2.0
eventbus_version=3.1.1
……
2、模式切换
在项目根目录下的gradle.properties
设置一个boolean的变量isModuleRun
,这个变量的作用就是控制业务组件Application
、Library
模式切换,当isModuleRun=true
,组件处于Application
可单独编译运行,反之则为Library
是一个依赖库,在模式切换过程同时还需处理每个Module的AndroidMainfest
文件的冲突,如下:
在每个业务Module下的build.gradle
下编写切换判断的代码处理模式切换
因isModuleRun
的值不同,Module的AndroidMainfest
文件内容也会有所不同,当Module处于Application
下,此时是独立应用,需要配置applicationId
,以及应用的启动页设置,而在Library
下则不需要这些,因为我们针对不同模式下引用不同AndroidMainfest
。
首页在Module的main
目录下创建一个module_run
目录单独存放Application
所需的AndroidMainfest
文件,接着在Module下的build.gradle
引入:
Lib下的AndroidMainfest文件内容
Application下的AndroidMainfest文件内容
到这里基本解决了模式切换的问题
3、资源冲突
从App空壳到基础组件,中间依赖很多其他组件,难免会有资源冲突的问题,在这情况下,建议在定义一个资源命名规范,大家统一遵守这个规范,能很好的避免资源冲突的问题,比如可以以Module名称作为前缀进行规范:
4、组件通信
组件间通信我们使用开源组件通信框架,比如阿里的ARouter,能很好的处理各组件间的跳转,并且同层次的组件间不会任何的依赖关系,实现了解耦的效果。使用如下:
在各组件下的build.gradle
添加依赖和配置
android {
defaultConfig {
...
//注意:这里每个业务组件都需要配置
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
//这里在Base基础组件添加依赖即可,其他组件无需添加
api "com.alibaba:arouter-api:${arouter_version}"
//注解依赖需要在各个组件中添加依赖
annotationProcessor "com.alibaba:arouter-compiler:${arouter_annotation_version}"
...
}
在BaseAppliction下初始化
private void initARouter() {
if (BuildConfig.DEBUG) { // 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,
必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this); // 尽可能早,推荐在Application中初始化
}
简单的使用,比如获取Fragment实例、启动Activity、拦截跳转页面
/**
* 路由管理类
* 命名规则:/模块名/特殊描述/目标页面名称(特殊描述可选)
* 需要登录的页面操作,带login_after字段
*/
public final class ARouterManager {
public static final String LOGIN_AFTER="login_after";
public static final String HomeFragment = "/home/HomeFragment";
public static final String CartFragment="/cart/CartFragment";
public static final String MeFragment="/me/CartFragment";
public static final String LoginActivity="/login/LoginActivity";
public static final String GoodsDetailActivity="/home/GoodsDetailActivity";
public static final String ShareActivity="/login/login_after/ShareActivity";
}
-------------------------------------------------------------------------------------
//Fragment路由路径定义
@Route(path = ARouterManager.HomeFragment) //定义路由路径
public class HomeFragment extends BaseFragment implements View.OnClickListener {
}
// Fragment实例获取
Fragment fragmet = (Fragment) ARouter.getInstance().build(ARouterManager.HomeFragment).navigation()
-------------------------------------------------------------------------------------
//Activty
@Route(path = ARouterManager.GoodsDetailActivity)
@SuppressWarnings("all")
public class GoodsDetailActivity extends BaseActivity {
}
//启动
ARouter.getInstance().build(ARouterManager.GoodsDetailActivity).navigation();
页面跳转拦截器,比如某些页面操作必须登录,我们可以先获取当前是否有登录,然后根据页面路由路径进行判断拦截页面跳转
/**
* 页面跳转拦截器
* 应用场景:如某些页面需要登录才可操作,可通过拦截器来统一处理跳转页面
*/
@Interceptor(priority = 7)
public class ARouterInterceptor implements IInterceptor {
Context mContext;
/**
* The operation of this interceptor.
*
* @param postcard meta
* @param callback cb
*/
@Override
public void process(final Postcard postcard, final InterceptorCallback callback) {
boolean isLogin = SpUtils.getBoolean(mContext, SpUtils.LOGIN_KEY);
String path = postcard.getPath();
if (!isLogin&&path.contains(ARouterManager.LOGIN_AFTER)){
//未登录
ARouter.getInstance().build(ARouterManager.LoginActivity).navigation();
callback.onInterrupt(null);
}else {
callback.onContinue(postcard);
}
}
/**
* Do your init work in this method, it well be call when processor has been load.
* 在路由初始化时会加载拦截器
* @param context ctx
*/
@Override
public void init(Context context) {
mContext = context;
Log.e("testService", ARouterInterceptor.class.getName() + " has init.");
}
}
上面是ARouter的一些简单用法,详细可以查看官方文档。
总结
组件化并没有一个放之四海皆准的通用方案,在我认为,只要实现各个业务模块、基础模块间解耦就是一个好方案,最起码相对之前单一工程来说,已经改善了很多了,效率肯定会有提升,只有根据自己项目实际情况,进行不断改造找到适合自己项目的设计方案。如果在现有的项目中进行组件化拆分,建议先把基础组件库进行剥离,紧接着再抽离一些共享数据组件(比如登录、分享组件等等),最后才对核心业务组件下刀拆分,在拆分的过程中千万别指望一口气全部拆分完,否则一不小心就会出现项目满堂红的情况,要做到边拆分边备份,避免代码丢失的危险。