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

所以我们上面的拆分结构是不足以让我组件化的,我们还需要添加一个路由,
包括路由的组件化拆分.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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容