Android 组件化

项目地址https://github.com/ccj659/clean-project-architecture

前言

随着业务的增多,迭代版本的增加,

模块化开发, 业务解耦, 业务独立进行测试,编译,运行,想想都惊喜~

如果不想忍受超长的编译时间,不想忍受类之间的强耦合,受够了满屏的不相干的文件,那么.....

为了你的"代码洁癖",还有项目的未来, 组件化, 势在必行.....

Android 业务组件化

项目地址https://github.com/ccj659/clean-project-architecture

类似于UML类图中聚合的概念,如下图所示,

image.png

组件可以自己行动,也可以组成一个整体运行.
关于基础组件,可以同时放在一个base包下, 也可以将base分不同的包,比如 数据库lib, 资源lib,等等...这些其实可以自己定义.

优点

便于开发,团队成员只关注自己的开发的小模块,降低耦合性,后期维护方便等。各自可以按照自己的代码风格开发,最后组装,成一个 app。

每个模块都可以打包成一个带版本号的@aar,对业务进行版本控制,降低了修改某一个业务造成其他业务受影响的风险.

与插件化的风险比较,组件化是几乎没有风险的,当下就可以做的一种架构.

不足

在模块间 数据交换,相互依赖,可能存在难题,路由器模块还不太成熟,问题各不相同,需要各自解决.

SHOW

zujianhua.gif

模块化 会遇到如下问题

  1. libarary和applicaiton 之间的转换

  2. 路由器,如何在拿不到类名的情况下,启动,模块间相互吊起服务. 最近路由器很多~
    目前的路由有阿里巴巴的,
    还有mzule的ActivityRouter.

  3. 代码解耦,作为线程间交互桥梁. 我用的是eventBus,作为事件总线,代替handler,

  4. 在集合app的最后,将每个模块打包成aar,减少编译时间.

组件化的构建步骤

请参照https://github.com/ccj659/clean-project-architecture 项目

image.png

1. 模块开发模式切换

1.在gradle.properties 增加一个变量

# true代表模块开发,false代表合并到主app.
#模式切换开关
isModule=false

2.在每个业务module的build.gradle里面添加

//根据isModule值进行切换 是否为lib或者app
if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

3.建立两个AndroidManifest.xml,进行切换

大家都知道,当项目是APPlication时候,需要有category为LAUNCHER的入口activity.
而当项目是lib的时候,不能存在入口activity.所以要分别建立两套AndroidManifest.xml,还要注意,如果想要保持主题样式通用, 主app项目下的theme,'ico','label'等等,在module中都不能存在.
进行如下配置.

image.png

4.建立两个AndroidManifest.xml,进行切换

    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
                java {
                    //release 时 debug 目录下文件不需要合并到主工程
                    exclude '**/debug/**'
                }
            }
        }
    }

5.业务组件不需要混淆代码.
一旦业务组件的代码被混淆,而这时候代码中又出现了bug,将很难根据日志找出导致bug的原因;

6.当ismodule开关为true时,每个module可独立运行.

image.png

2. 数据路由

首先,考虑到解耦, 可以把ARouter的所有跳转都进行封装.以防以后更换路由器.

这里,module内,可以用传统方式传递和用路由器传递都行.

3. 资源重复

  1. module1和module2都依赖base,则gradle在编译期间,会自动去重,我们不需要管.

  2. 资源名重复,解决1:在编码的时候 添加resourcePrefix "video"+"_",强制人员添加前缀(但是对drawable不支持,需用户自己增加前缀).

  3. 解决2:将资源统一放在一个module(比如Base中),但是编译会增加时间.(不太复合资源解耦 原则)

  4. 如果是用provide代替complile

4. 代码隔离-(面向接口编程)

基础库Base

  1. 网络库 我们项目用的volley(10年开始的项目迭代至今),
  2. 资源库 基础mipmap,drawable资源等等
  3. BaseClass BaseActivity,BaseBean,BaseAdapter等等.
  4. weight组件 共同的自定义view,或者第三方view.
    ....

组件Module

组件间通信 IProvider

请参考ARouter的文档的通过依赖注入解耦:服务管理(一) 暴露服务 进行~

举个例子, 在我的项目中clean-project-architecture中,videoModule的拍照功能需要调用loginmodule的登录功能,按照上述例子,就可以实现.

// 声明接口,其他组件通过接口来调用服务
/**
 * 示例:子模块间调用方法
 * Created by chenchangjun on 17/8/14.
 */

public interface LoginModuleService extends IProvider {


     boolean checkLoginState();

}


// 实现接口
/**
 *     * 实现接口,
 * Created by chenchangjun on 17/8/14.
 */
@Route(path = RouterConstants. LOGIN_SERVICE_IMPL)
public class CheckLoginService implements LoginModuleService{

    /**
     * 实例化服务,面向接口编程
     * @return
     */
    @Override
    public boolean checkLoginState() {
        //可自行在loginModule
        return false;
    }

    @Override
    public void init(Context context) {

    }
}


//另外一个module调用,(由接口进行隔离)
    private void takePhoto() {
        if (loginModuleService.checkLoginState()){ //模拟模块间通信,调用登录服务:如果登录就开始下一步.
            startTakePhoto();
        }else {
            Toast.makeText(this,"请登录",Toast.LENGTH_SHORT).show();
        }
    }


宿主App

每个module都有Application ,这里,为了方便, 将共同的东西抽取出来,放在了basemodule的BaseApplicaiotn中.
当遇到每个module可能都要有自己初始化的方法,我们可以在每个module 附带一个application.

遇到问题

1.Butterknife 的bindview()方法,library的不能存在,原因是在app和library切换的时候,注解上的变量必须是static final, library不能存在switch().

Butterknigher libarary不能用,这篇文章不错

问题:

image.png

解决

  1. 上述问题可以,将R改为R2如上图第二个变量所示.重新clean,即可.

  2. butterknife的onclick事件,用下面的方式处理即可.

  3. 注解问题,就像用dragger.xutils等等,能不能这种方式处理,还有待测试.

//package com.ccj.login.ui.login;

  /** 
     * click方法中同样使用R2,但是找id的时候使用R,
     * ibrary中是不能使用 switch- case 找id的,原因:http://www.jianshu.com/p/89687f618837
     */
    @OnClick({R2.id.iv_cancel, R2.id.btn_login, R2.id.btn_register})
    public void onClick(View view) {
        int i = view.getId();
        if (i == R.id.iv_cancel) {
            finish();
        } else if (i == R.id.btn_login) {
            //mPresenter.login(tvPhone.getText().toString(), tvPassword.getText().toString());
            Toast.makeText(this,"登录测试",Toast.LENGTH_SHORT).show();
        } else if (i == R.id.btn_register) {
            navigateToRegister();
        }
    }

2.目前,路由器ARouter 没有解决onActivityResult的fragment分发问题.

问题:

当你再fragment上 进行路由

  ARouter.getInstance().
                build(RouterConstants.VIDEO_MUDULE_ACTIVITY).
                withString(Constants.START_LOGIN_WITH_PARAMS, "I am params from MainActivity").
                navigation();

在fragment中的onActivityResult是接收不到数据的,ARouter会在activity中调用该方法.

解决

在BaseActivity中重写onActivityResult方法.让子类继承即可.

 /**
     * 解决fragment onActivityResult不调用
     *
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        FragmentManager fm = getSupportFragmentManager();
        //if (index != 0) {
        if (fm.getFragments() == null) {
            Log.w(TAG, "Activity result fragment fragmentIndex out of range: 0x"
                    + Integer.toHexString(requestCode));
            return;
        }
        for (int i = 0; i <fm.getFragments().size() ; i++) {
            Fragment frag = fm.getFragments().get(i);
            if (frag == null) {
                Log.w(TAG, "Activity result no fragment exists for fragmentIndex: 0x"
                        + Integer.toHexString(requestCode));
            } else {
                handleResult(frag, requestCode, resultCode, data);
            }
        }
        return;
        //}

    }

    /**
     * 递归调用,对所有子Fragement生效
     *
     * @param frag
     * @param requestCode
     * @param resultCode
     * @param data
     */
    private void handleResult(Fragment frag, int requestCode, int resultCode,
                              Intent data) {
        frag.onActivityResult(requestCode, resultCode, data);
        List<Fragment> frags = frag.getChildFragmentManager().getFragments();
        if (frags != null) {
            for (Fragment f : frags) {
                if (f != null)
                    handleResult(f, requestCode, resultCode, data);
            }
        }
    }
    

3 各种编译处理插件,可能会出现问题

因为Arouter是编译期间 执行,所以当你的项目集成dragger2,butterknife,xutils,databinding可能会出现问题.需要各自排查

总结

组件化是用gradle作为组间切换工具,用Arouter作为跳转路由器 的一种 框架.

在开发中, 组件化,有利于模块业务解耦,让每人负责的业务相互独立.
在后续开发中,我们可以将不同的组件模块lib分别独立,需要的时候分别进行依赖即可.

相关代码实现请查看项目
https://github.com/ccj659/clean-project-architecture

参考:

ARouter 类似于Spring的控制反转IOC.路由分发

创建 Android 库

http://blog.csdn.net/guiying712/article/details/55213884

吴小龙 Android 组件化探索与思考

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

推荐阅读更多精彩内容