项目实战之组件化架构

前言

关于什么是组件化、为何要进行组件化以及实施组件化的基本流程网上一搜一大把,这里不作过多说明,不了解的话能够Google一下。这里主要记录一下组件化开发的一些心得和踩的一些坑。vue

先看一下项目结构图

结构很简单,有一个公共的基础module类commonlibrary来处理一些公共的东西,好比第三方库的依赖,基类封装,工具类等。中间层是各个独立的业务模块,各个模块之间互相隔离。最下面是app的壳,主要配置签名打包什么的。具体能够看一下 demo

组件化实施步骤

一、设置module是否做为组件的开关

在gradle.properties文件里定义一个常量IsBuildApp = false,表示是否把组件module做为单独的app运行。定义好了这个常量后,在项目的任何一个gradle文件里均可以读取到这个值,那么就用这个值来做为module组件是否须要单独运行的开关。java

// 在module组件的gradle里配置以下,gradle.properties 中的数据类型都是String类型,这里须要作一下转换
if (IsBuildApp.toBoolean()){
    apply plugin: 'com.android.application'
}else {
    apply plugin: 'com.android.library'
}
复制代码
二、组件module的清单文件AndroidManifest合并问题

咱们知道android的四大组件、权限等都是须要注册的,当module单独运行的时候,确定须要一个清单文件注册组件和申请权限,可是当module做为app的一个子组件存在的时候,清单文件是要合并到app的壳工程中的,这个时候若是每一个module都有本身的启动页面和自定义application的话,就会引发冲突。android

为了解决这个问题,那就须要根据module是否须要单独运行来配置不一样的清单文件。在java同级目录新建independent目录,在此目录下建立项目module须要单独运行的清单文件和application。而后在module的gradle文件里指定清单文件路径,代码以下:git

// 在android领域里指定清单文件的路径

sourceSets {
    main {
        if (IsBuildApp.toBoolean()) {
            // 单独做为app运行的清单文件,这里能够添加启动页面、自定义application等。
            manifest.srcFile 'src/main/independent/AndroidManifest.xml'
        } else {
            // 做为组件的清单文件
            manifest.srcFile 'src/main/AndroidManifest.xml'
            //release模式下排除independent文件夹中的全部Java文件
            java {
                exclude 'independent/**'
            }
        }
    }
}
复制代码

这样配置完成之后,做为组件的清单文件是不能有本身的启动页面、application、appname等属性的,下面看一下完整的配置:github

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.article.demos.vue">

    <application android:theme="@style/AppTheme">

        <activity android:name=".ui.VueActivity" />

    </application>
</manifest>
复制代码

下面看一下独立运行模式下的清单文件:api

// 做为独立app运行的清单文件,注意这里我设置了主题,否则的话会报错。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.article.demos.main">

    <application android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
复制代码

独立运行的话,就和正常的app清单文件同样,要有启动页面,application标签能够添加label、icon、自定义application等,就很少说啦。微信

三、全局Application的问题

在commonlibrary中建立自定义application,由于其余的module都依赖这个module,因此其余的module均可以获取到这个全局的application。另外,组件在独立运行模式下的application,继承咱们自定义这个BaseApplication就能够了。由于咱们在release模式下,排除了全部independent文件夹下的java文件,因此做为组件运行时,并不会产生application的冲突,配置以下:架构

sourceSets {
    main {
        if (IsBuildApp.toBoolean()) {
            manifest.srcFile 'src/main/independent/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            //release模式下排除independent文件夹中的全部Java文件
            java {
                exclude 'independent/**'
            }
        }
    }
}
复制代码
四、重复依赖三方库的问题

为了不重复依赖三方库的问题,咱们的三方库依赖统一放在commonlibrary的module中,这样既能够避免重复依赖,又方便管理。而后咱们在app的module里,以下引用便可:app

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    if (IsBuildApp.toBoolean()) {
        implementation project(':commonlibrary')
    } else {
        implementation project(':androidmodule')
        implementation project(':vuemodule')
        implementation project(':kotlinmodule')
        implementation project(':javamodule')
    }
}
复制代码
五、资源冲突问题

资源冲突主要是指各个module里的资源文件名冲突的问题,若是命名同样,合并的时候便会产生冲突。ide

解决冲突主要有两个解决方案,一个是约定规则,好比资源名约定都以module名开头。

方案二是经过gradle脚原本设置,在各个组件的gradle文件里添加以下代码:

resourcePrefix "module名称_"
复制代码

可是这种配置有限制,好比只能限定xml里的资源,因此并不推荐这种方式。

六、组件间跳转

由于组件是相互隔离的,咱们并不能显式跳转,这里咱们选用阿里巴巴的Arouter路由跳转,项目的地址github.com/alibaba/ARo…

这里须要特别说明一下,须要跳转的目标module须要引入arouter的注解处理器,不然没法处理router注解会出现路径不匹配的问题:

annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
复制代码

同时,改module的defaultconfig里也别忘记配置moduleName

javaCompileOptions {
        annotationProcessorOptions {
            arguments = [ moduleName : project.getName() ]
        }
    }
复制代码
七、跨module交互

跨moduel交互通常是指module间通讯和module间的相互调用。module间通讯这里选用eventbus,很简单,就不过多说明了。

下面说一下同级module直接的通讯,好比我在任何一个页面要调用loginModule里的微信登陆方法,由于各个module是互相独立的,互不依赖,想要直接调用基本不可能。目前网上发现有两种解决方案,一个是写一个反射工具类,经过反射获取到要调用的类,而后调用相应的方法。另外一个是经过commonModule作一下桥接,了解更多能够参考这里。不过感受用Arouter能更优雅的实现,下面具体讲一下利用arouter来实现。

首先,在公共module里建立一个接口IService

public interface IService extends IProvider{
    String wxLogin();
}
复制代码

接口里定义一个微信登陆的伪代码,而后在咱们的登陆组件里,实现该接口并添加route注解

@Route(path = Constant.WX_LOGIN)
public class WxTest implements IService{

    @Override
    public void init(Context context) {

    }

    @Override
    public String wxLogin() {

        return "wxlogin";
    }
}
复制代码

其中 Constant.WX_LOGIN是我定义的一个字符串常量

public static final String WX_LOGIN = "/wx/login";
复制代码

以上两步就把工做作完了,下面只须要在须要调用的页面调用登陆就好了。首先,咱们获取到IService

/**
     * 推荐使用方式二来获取IService
     */
    // IService iService = (IService) ARouter.getInstance().build(Constant.WX_LOGIN).navigation();
    IService iService = ARouter.getInstance().navigation(IService.class);
复制代码

拿到IService后,就能够放心大胆的调用登陆方法就好了。

mBinding.btLogin.setOnClickListener(v -> {
        String s = iService.wxLogin();
        Toast.makeText(getContext(), s, Toast.LENGTH_SHORT).show();
    });
复制代码
八、fragment的组件化

通常的项目首页都是一个activity和多个fragment组成。因为组件间的隔离,咱们在首页里怎么获取到其余组件里的fragment呢?开篇的两个参考文章分别使用了两种不一样的方式,有兴趣的朋友能够看看。各有利弊吧,一个是查询全部,太耗时。一个是直接反射获取,可是好像有点违背组件隔离,须要知道fragment的全路径。

这里我参考了《Android组件化架构》一书,使用arouter来获取。其实三种方式获取的原理同样,都是经过反射。咱们看一下arouter的注解的源码就知道:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {……}
复制代码

能够看到Route注解的retention是CLASS,也是经过反射来获取。

九、遇到的一些坑

(1)使用dataBinding的话,每一个module的gradle文件里都要加上dataBinding的支持,不然没法生成相应的binding类

// 每一个module都加上dataBinding的支持,不然没法生成相应的binding类
    dataBinding {
        enabled = true
    }
复制代码

(2)java8的支持同样要每一个module都要单独配置

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
复制代码

(3)升级到as 3.1.2后,出现没法访问TaskStackBuilder的问题

检查一下你的support包,将你的support包更新到27或以上便可。

(4)若是使用有自定义注解annotation的话,若是编译报错 Annotation processors must be explicitly declared now...,那么在commonlibrary的gradle文件的defaultConfig里添加以下代码:

// Annotation processors must be explicitly declared now
    javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
复制代码

(5)若是你组件化开发,子module中没法使用butterknife的话,网上自行搜解决方案吧(🤦‍♀️)

关于为什么出现这个问题,推荐一篇博文R.java、R2.java是时候懂了

(6)其余问题本篇博客会持续更新……

2018年6.15更新………………………………………………………… 编译报错

Multiple dex files define Lcom/alibaba/android/arouter/routes/ARouter$$Group$$module
复制代码

通常网上说是依赖版本冲突,其实这个问题是不一样module之间有相同分组致使的问题,好比a模块 path = "/message/a",b模块 path = "/message/b",有相同的message分组,修改为不同的就能够了。

2018.8.20更新…………………………………………………………

最近在用kotlin和java混合开发,发现原有java页面跳转新写的kotlin页面 arouter 页面跳转的时候报异常提示 There is no route match the path……,此时参考官方文档便可解决,

// 在kotlin的module中添加插件
apply plugin: 'kotlin-kapt'
// 依赖里 使用kapt 引用
dependencies {
    compile 'com.alibaba:arouter-api:x.x.x'
    kapt 'com.alibaba:arouter-compiler:x.x.x'
    ...
}
复制代码
2018.8.22更新…………………………………………………………

遇到了一个很蛋疼的问题,在纯java写的module里经过arouter跳转到另外一个module里的kotlin页面的时候,发现setcontentview方法无效,页面什么都不显示。调试了半天,发现是页面的xml布局文件和一个空的xml布局文件重名了,致使kotlin页面加载了空页面的布局,在此记录一下,好尴尬。

最后附上完整的demo地址,若是对你有帮助麻烦start鼓励一下,你的鼓励是我前进的动力。

相关文章
相关标签/搜索