Android模块化实战:学海题舟模块化

为什么要模块化

当一个App用户量增多,业务量增长以后,就会有很多开发工程师参与同一个项目,人员增加了,原先小团队的开发方式已经不合适了。原先的一份代码,现在需要多个人来维护,每个人的代码质量也不相同,在进行代码Review的时候,也是比较困难的,同时也容易会产生代码冲突的问题。同时随着业务的增多,代码变的越来越复杂,每个模块之间的代码耦合变得越来越严重,解耦问题急需解决,同时编译时间也会越来越长。人员增多,每个业务的组件各自实现一套,导致同一个App的UI风格不一样,技术实现也不一样,团队技术无法得到沉淀。

模块化的好处

  • 多团队并行开发测试。
  • 模块间解耦、代码复用。
  • 可单独编译打包某一模块,提升开发效率。

模块化架构设计

整体架构预览(简单版)

xh_framework

三层架构,底层通用模块,上次为业务模块层,最上层为应用层,应用层来装载各个模块。

整体架构预览(详细版)

xh_module

在三层架构的基础上做细节拆分。底层分为学海库,第三方库,和模块通讯工厂。上次包含网络和数据层,工具层等。

模块设计预览

xh_module_design

复杂版组件化整体架构方案,包含首页模块,作业模块,排行榜模块,诚信分模块,我的模块。

模块化代码实现

接口隔离设计

  • 设计接口分为两层接口。第一层为获取各个模块接口的工厂层,内置DataModule来解决模块之前的数据传递问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public interface ModuleFractory {
    /**
    * 初始化
    * @param context
    */
    void init(Context context);

    /**
    * 获取数据模块
    * @return
    */
    DataModule getDataModule();

    /**
    * 获取用户模块
    * @return
    */
    UserModule getUserModule();
    }
  • 第二层为模块内部用来暴露当前模块所要提供给外部模块使用的函数层接口,通过注解Module来找到此接口对应的实现类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Module("com.xxx.UserModuleImpl")
    public interface UserModule extends BaseModule {

    boolean isLogin();

    UserInfo getUserInfo();

    //可以通过Router的方式跳转
    void startLoginActivity();

    }

模块间数据变化

数据传递可以使用模块化自带的DataModule模块来处理,也可以使用EventBus或者广播来处理。推荐使用DataModule。伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Module("com.xxx.DataModuleImpl")
public interface DataModule extends BaseModule {

<T> void addDataUpdateListener(OnDataUpdateListener<T> listener);

<T> void removeDataUpdateListener(OnDataUpdateListener<T> listener);

void notifyDataChanged(Object data);

void removeAllDataUpdateListener();

interface OnDataUpdateListener<T> {
void onDataUpdate(T data);
}
}

拆分应用

确定依赖关系

各个模块依赖于Common Library,App依赖于各个模块。

提取公共配置

将公共设置项提取到项目的build.gradle文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ext {
isRun = false
compileSdkVersion = 26
minSdkVersion = 15
targetSdkVersion = 26
versionCode = 1
versionName = "1.0"

commDependencies = [
appcompat : "com.android.support:appcompat-v7:26.1.0",
constraint_layout : "com.android.support.constraint:constraint-layout:1.0.2",
butterknife : "com.jakewharton:butterknife:8.8.1",
butterknife_compiler: "com.jakewharton:butterknife-compiler:8.8.1",
]
}

common模块的依赖情况如下

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
//公共依赖
implementation commDependencies.appcompat
implementation commDependencies.constraint_layout
implementation commDependencies.butterknife
annotationProcessor commDependencies.butterknife_compiler
//模块库
implementation project(':xh_module')
//路由库
implementation project(':xh_router')
}

模块gradle配置

1
2
3
4
if (isRun)
apply plugin: 'com.android.application'
else
apply plugin: 'com.android.library'

标识是否是作为应用还是作为一个依赖库

1
2
3
4
5
apply plugin: 'com.jakewharton.butterknife'
dependencies{
implementation project(':xh_common')
annotationProcessor commDependencies.butterknife_compiler
}

butterknife插件引用,方便使用R2代替R。

1
2
3
4
5
6
defaultConfig {
resourcePrefix "user_"
if (isRun)
applicationId "com.xh.user"
...
}

添加资源前缀,如果资源没有加此前缀,则会报红,但不影响运行。
单独运行时,需要添加应用ID。

1
2
3
4
5
6
7
8
9
10
11
12
sourceSets {
main {
if (isRun.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
exclude 'debug/**'
}
}
}
}

根据是否可运行来使用不同的AndroidManifest文件。避免Manifest merger failed with multiple errors这个错误

R文件替换为R2

在使用ButterKnife这种框架时,比如@BindView(R.id.titleText)时,注解中的值必须是final类型的才可以,否则会提示Attribute value must be constant。因为编译器生成的R文件不是final类型的,所以需要使用R2来完成。当然,如果用findviewbyId的形式,就没有这个问题了。

项目右击 => 使用Replace in Path功能,选择Modlue => xh_user。


注:onClick中不要使用switch语句。不要再方法内部使用R2。

导入替换

  • R文件com.xh.abrutch.R要被替换成com.xh.common.R
  • 包名替换com.xh.abrutch 要替换成 com.xh.common

筛选资源文件,过滤模块中不需要的资源文件。

使用Android Lint工具检测res目录下未使用的资源文件。包括anim、color、drawable、layout、mipmap、attrs、colors、dimens、strings、styles

您的支持是我原创的动力