Android组件化
最近刚刚实践了组件化,开始听到组件化的时候觉得有点畏惧,比较陌生,但是真正去做的时候并没有你想象中的那么困难。当然,这也是我第一次组件化,如果有什么不对你别骂我。
要Demo的请戳这里,安卓组件化Demo
什么是组件化?
组件化,从字面上看,就是把一个完整的东西拆分成若干个小组件,然后拼接成一个完整的实体。就好比机器人,都是由头部,躯干,手脚等组件拼装起来的。但是,我们这一组件和这个机器的组件还是有区别,区别在哪里呢?机器人的头部,躯干,手脚单独任何一个组件都是无用的,无法实现或者说完成一个指令。我们的组件化里面的任意一个功能组件,都是应该可以独立编译运行并且可以一起组装成一个完整的应用编译运行。所以,机器人的这个例子不那么严谨,看过数码宝贝的同学(暴露年龄了)应该知道奥米加兽,由战斗暴龙兽。。咳咳,扯远了,大概就是这么个意思。
为什么要组件化?
- 项目庞大,业务复杂,组件化可以更加清晰的梳理业务逻辑
- 团队开发,耦合度高,组件化可以让专门的人开发维护某个组件
- 项目成熟,功能丰满,组件化可以快速的将部分功能模块抽离成独立的应用
- 项目庞大,代码累积,这个时候最为要命,编译运行一次几分钟就过去了,组件化可以单独编译某个模块,大大的提升了开发效率
怎么实现组件化?
写在前面
组件化属于最好在项目之初就开始架构,中途架构可能会遇到比较糟心的问题(博主本人),之前代码不一定都是你写的,经多人之手很难整理,有时候不得不维护两套,保留之前的。
首先,组件化拆分后的结构
这是我自己总结的拆分,可能会有差别,可供参考
组件化简单拆分.png
从上图可以看出,BASE其实也是一个组件,但是这个组件里面没有业务逻辑,只是一些基类和公共资源的整合。同时BASE还依赖了一些其他组件,HTTP网络访问组件,UI组件和其他组件,按照这个逻辑其他一些第三方库依赖也应该在BASE中。
再往上看,我们的业务组件(A组件,B组件...N组件)都依赖了BASE组件,但是A组件,B组件...N组件之间没有依赖。为什么业务组件可以依赖BASE组件,而业务组件之间不依赖呢?不是不依赖,而是不应该依赖,我们组件化的初衷就是分离模块,使他们之间不产生依赖关系。有同学要问了,那么,我A组件有可能涉及到B组件里面的交互怎么办呢?先不着急,这个要等到下面说了,现在我们先理顺这张图。刚刚说组件不应该互相依赖,但是业务组件确实依赖了BASE组件,BASE组件还依赖了一些其他组件,这是为什么呢?因为,BASE组件和HTTP组件或者UI组件这些组件中根本不存在任何业务和逻辑关系,换句话说,无论把这几个组件放在哪个项目当中都是OK的。
最上面一层就是我们的APP壳了,APP壳中其实包含的东西很少,但是要整合所有的业务组件,然后编译打包成一个完整的项目。所以APP壳里面依赖了所有的业务组件,值得一提的事,因为APP壳里面依赖了所有的业务组件,但是APP里面也不应该直接使用某个业务组件里面的东西。
组件化过程
简单来说,一个组件就是一个Module,那怎么做到即可单独编译运行又可以整合到APP中呢?其实很简单,因为每个Module都有这样一个apply plugin: 'com.android.application'或apply plugin: 'com.android.library'配置,所以只要动态修改这个就可以让这个Module作为一个APP或者作为一个library存在了。
但是,当我们一个项目有很多个组件的时候我们不可能一个个的去修改。偷懒是我们光荣的程序员的特质,这个问题其实就是一个if--else能够解决的,但是我们需要一个全局变量,这个变量可以在gradle.properties这个文件中设置。
单独和分离配置.png
如上图,我们在gradle.properties这个文件中添加了一个Boolean类型的参数isDebug,然后我们就可以在Module的gradle中添加这样一段代码就OK了。要想切换APP和library就只需修改isDebug,无需一个一个Module去修改了(在gradle中都是字符串类型所以isDebug要强转从Boolean类型)。
if (isDebug.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
这个时候我们又会迎来一个新问题,当一个Module作为APP编译运行时,它的AndroidManifest是需要自定义的Application的,因为一些第三方库或者其他一些东西需要在Application中初始化。我们可以在BASE中定义一个Application,在BaseApplication中初始化,每个Module的Application都继承BaseApplication或者直接使用BaseApplication。说回这个新问题,APP和library的AndroidManifest是不一样的,那我们这个时候也要想上面一样通过判断来选择使用哪个AndroidManifest,还是在这个Module的gradle添加一段代码(值得注意的是,如果你copy这份代码,导包后manifest可能会变成大写开头,请一定要用小写的)。
sourceSets{
main{
if (isDebug.toBoolean()){
manifest.srcFile 'src/main/java/debug/AndroidManifest.xml'
}else{
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
代码其实就是引用哪个xml文件,但是这里出现了两个路径,其中不是debug的xml是自动生成的,debug中的是我们手动创建的,所以我们还要在src/main/java下新建一个debug的package,然后把src/main中的AndroidManifest.xml复制一份到debug中。
不同的AndroidManifest.png
就这么简单,配置完成后只需要修改isDebug,就可以在APP和library中自由切换了。
如果就这样,你会遇到很多糟心的问题,比如你的APP中有一个activity_main.xml的布局文件,同时你的Module中也有这么一个文件且两个文件中都有一个相同的id的控件,你就会遇上问题了。当然不仅仅包括这个问题,还有其他很多资源冲突的问题也可能发生。
那么,我们该如何解决这个问题呢?有一个方法,但不算是解决,算是预防。那就是我们规定一种方式,杜绝资源冲突的问题。比如:im组件里面我的资源文件都是以im_这种方式开头命名的,share组件里面我们都是以share_这种方式开头命名的。当然,为了让你记住这个约定,可以在Module的gradle中进行如下配置:
resourcePrefix "im_"
资源冲突处理1.png
再当你创建一个layout时,就会默认以你设置的约定开头,这样就可以有效的避免资源冲突了(好像只有在android的目录结构下才有效,见下图)。
资源冲突处理2.png
组件之间交互
通过以上操作,基本已经确立了组件化的框架,随着组件多起来,组件之间也会存在一些交互(上文提到的A组件涉及到B组件的一些交互)。因为我们的组件间是不存在互相依赖的,那我们该如何进行组件间的交互呢?
这个时候我们需要引入一个Router的概念了,我们这些交互都需要经过Router中转
所以我们上面的拆分结构是不足以让我组件化的,我们还需要添加一个路由,
包括路由的组件化拆分.png
和之前的相比只是多了一个ROUTER,那我们怎么利用ROUTER来实现组件间的交互和跳转呢?这个时候阿里爸爸就要登场了,有一个叫做 ARouter 的东西,可以协助我们实现路由。Ok,你可以参考他的文档,写的已经比较清楚,我这里也会说明一下。
-
Step1 : 了解路由
建一个router的Module,这个Module里面什么也不做只暴露一些服务,配置gradle如下(需要注意的是,图中红框框内的东西最好在你用到ARouter的Module里面都配置一下)。
路由的gradle配置.png -
Step2 : 依赖并初始化
BASE组件中依赖ROUTER,并在BaseApplication中初始化,参照 ARouter 。 -
Step3 : 暴露服务
在我们的ROUTER组件中暴露一些服务,创建一个SayHelloService如下
package com.yxr.router;
import com.alibaba.android.arouter.facade.template.IProvider;
/**
* Created by yxr on 2017/12/9.
*/
public interface SayHelloService extends IProvider {
String sayHello(String name);
}
- Step4 : 实现这个服务
假设我这服务需要在IM这个组件中实现,那么我们创建一个SayHelloServiceImp如下
package com.yxr.im;
import android.content.Context;
import com.alibaba.android.arouter.facade.annotation.Route;
import com.yxr.router.SayHelloService;
/**
* Created by yxr on 2017/12/9.
*/
@Route(path = "/im/sayHelloService")
public class SayHelloServiceImp implements SayHelloService{
@Override
public String sayHello(String name) {
return "hello," + name;
}
@Override
public void init(Context context) {
}
}
- Step5 发现服务
假设我们需要在SHARE组件中使用到这个服务,只需要发现这个服务(需要注意,单独运行某个组件时,其他组件的并不会编译进去,所以导致 ARouter 无法正常工作,所以该判断的地方还是需要判断一下),如下:
package com.yxr.share;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import com.alibaba.android.arouter.facade.annotation.Autowired;
import com.alibaba.android.arouter.facade.annotation.Route;
import com.alibaba.android.arouter.launcher.ARouter;
import com.yxr.base.ui.BaseActivity;
import com.yxr.router.SayHelloService;
/**
* Created by yxr on 2017/12/9.
*/
@Route(path = "/share/shareActivity")
public class ShareActivity extends BaseActivity implements View.OnClickListener {
@Autowired()
SayHelloService sayHelloService;
// 建议使用这种方式,因为接口是可以被多实现的,除非你100%确定SayHelloService这个接口只被一个实现
// @Autowired(name = "/im/sayHelloService")
// SayHelloService sayHelloService;
public ShareActivity() {
ARouter.getInstance().inject(this);
}
@Override
public int contentView() {
return R.layout.share_activity;
}
@Override
public void initView(@Nullable Bundle savedInstanceState) {
}
@Override
public void initListener() {
findViewById(R.id.btnHello).setOnClickListener(this);
findViewById(R.id.btnJumpIm).setOnClickListener(this);
}
@Override
public void initData() {
setCommonTitle(getClass().getSimpleName());
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btnHello){
// 为什么要判断不为空呢?因为组件间没有互相依赖,当你单独运行某个组件时
// 另外一个组件并没有编译进来,所以会发现不了实现这个接口的服务
if (sayHelloService != null){
toast(sayHelloService.sayHello("组件化"));
}
} else if (id == R.id.btnJumpIm){
ARouter.getInstance().build("/im/imActivity").navigation();
}
}
}
组件间的跳转,Fragment获取
其实上文已经使用到了这里要讲的东西,和需要注意的地方。具体实现起来也比较简单,有了上面的基础后,我不在累述,需要了解更多的可以参考 ARouter 。
哦,对了,附上几张GIF图演示,源码,之后我也会整理一份分享到GIT上。
单独编译和整合为APP.png 组件整合为APP.gif Share组件单独编译运行.gif Im组件单独编译运行.gif
最后BB两句
这个文章仅供参考,如果你喜欢可以给我小发发,如果有批评指教也请你温柔提点指出。最后,附上一波彩蛋(注意事项之踩坑记录)。
- 在Module中资源文件判断不要用switch,用if,如下:
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btnHello){
} else if (id == R.id.btnJumpIm){
}
}
-
manifest问题
No signature of method: static org.gradle.api.java.archives.Manifest.srcFile() is applicable for argument types: (java.lang.String) values: [src/main/java/debug/AndroidManifest.xml]<ahref="openFile:D:\ProgramFiles\Android\workspaces_2\MyApplication\wyim\build.gradle">Open File</a>
出现这个异常时把Module中的Manifest.srcFile() 替换成 manifest.srcFile() -
ARouter问题
用到ARouter的Module里面都配置一下上文说到的配置;
记得在Application中初始化ARouter;
单独运行某个组件时,其他组件的并不会编译进去,所以导致 ARouter 无法正常工作,所以该判断的地方还是需要判断一下; -
资源冲突问题
利用gradle的 resourcePrefix "share_" 约定资源文件命名规范 -
ButterKnife问题
在组件化的时候一定要一开始就注意ButterKnife的问题,具体解决方案请查看 ButterKnife问题解决 -
其他
需要可爱认真的你自己去踩踩
要Demo的请戳这里,安卓组件化Demo