Android组件化方案实践与思考

Demo地址:https://github.com/751496032/ComponentDemo

效果图:


效果图

背景

Android从诞生到现在,不知不觉的走过十多个年头了,也产生了很多App,随着项目的推进不断的迭代,而App也从最初的单一功能演变成多任务功能,各种业务的错综复杂,开发人员也不断的增加,如果架构不做调整优化,会给开发带来很大的困难:

  • 各种业务代码耦合性及高,代码臃肿会越来越高,不利于团队间协同开发,维护成本也高;
  • 降低开发效率,工程的编译运行时间及长,在单一工程下,每修改一小处都要运行整个项目,导致非常耗时。
  • ……

基于代码耦合性问题,App的设计架构也不断的演变,从最初的MVC,到现在主流的MVP、MVVM,这些模式也确实起到代码解耦的效果,但还是很大局限性的;各业务间的耦合、运行效率问题都还是存在的;于是组件化思想就诞生了,组件化有如下优势:

  • 业务间代码互不干扰,解耦性好,代码复用性也高;
  • 各个组件能单独生成apk,可以单独调试,降低了编译运行时长。

组件化思想

组件化就是把单一工程的app分成多个Module,每个Module就相当于一个组件,而这些组件是不需要相互依赖的,可根据开发需求,自由将各个组件进行ApplicationLibrary模式切换进行调试开发。

上面是组件化基础结构图,从上向下分为三部分,分别是app空壳、功能组件(业务组件)、基础组件;

  • App空壳只有一个组件就是App组件,需要依赖于各个业务组件,最终上线的就是App统筹所有的业务组件打包生成的;
  • 功能组件又称之业务组件,各个组件间并没有依赖关系,除了Login组件外,把Login组件单独分开的原因是,我认为基本上所有的业务组件都需要登录行为才可以操作,不排除少数业务组件是不需要的,于是干脆把所有的业务组件全部依赖于Login组件,在这里Login组件其实是一个共享组件;
  • 基础组件,这个很好理解就是我们封装的基础库,比如网络、路由、推送、图片等等

组件化需解决的问题

  • 模式切换,如何使每个Module在ApplicationLibrary间自由切换;
  • 依赖关系,如何处理每个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,这个变量的作用就是控制业务组件ApplicationLibrary模式切换,当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的一些简单用法,详细可以查看官方文档。

总结

组件化并没有一个放之四海皆准的通用方案,在我认为,只要实现各个业务模块、基础模块间解耦就是一个好方案,最起码相对之前单一工程来说,已经改善了很多了,效率肯定会有提升,只有根据自己项目实际情况,进行不断改造找到适合自己项目的设计方案。如果在现有的项目中进行组件化拆分,建议先把基础组件库进行剥离,紧接着再抽离一些共享数据组件(比如登录、分享组件等等),最后才对核心业务组件下刀拆分,在拆分的过程中千万别指望一口气全部拆分完,否则一不小心就会出现项目满堂红的情况,要做到边拆分边备份,避免代码丢失的危险。

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

推荐阅读更多精彩内容

  • Android组件化项目地址:Android组件化项目AndroidModulePattern Android组件...
    半灬边灬天阅读 2,865评论 4 37
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,574评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,482评论 2 59
  • 不怕跌倒,所以飞翔 组件化开发 参考资源 Android组件化方案 为什么要组件化开发 解决问题 实际业务变化非常...
    笔墨Android阅读 2,903评论 0 0
  • 作者:丁香空结 飞雪连天射白鹿,笑书神侠倚碧鸳。这两句对联概括了金庸的14本小说,也是童年的记忆。书中塑造了许多令...
    一个人的改变阅读 408评论 0 0