Android组件化开发填坑记录

Android组件化开发通常也说模块化开发,使用Android Studio做开发时,通常组件化的实现就是在一个工程项目中同时包含若干个application模块和若干个library模块,还会引用一些第三方类库或者依赖。每一个application模块通常可以编译成一个应用,而library模块一般不用于生成可执行的应用,而是生成jar/aar类库,或者被同一项目下其他的library或application模块引用。

模块化的原因

模块化会增加编译负担,造成一些不必要的错误,所以,对于功能单一的应用,自然没有必要做模块化,所有代码写在一个app模块就行。但是对于复杂的业务场景,比如需要同时维护多个功能类似,但又有一些差别的应用,或者应用的功能模块需要经常进行定制化裁剪,或者多人协同开发功能模块,这些场景下使用组件化可以方便就行功能复用和裁剪,每个功能模块独立开发,互不干扰,提高开发效率。

组件化遇到的问题

一.第三方类库依赖问题

1.远程依赖问题

一般来说,远程依赖是最简单的,只需要配置路径即可,例如:

1
compile 'com.android.support:design:25.3.1'

如果在library模块中引用了一次,引用该library的其他模块无需声明,也可以使用这个远程依赖。若两者使用了不同版本的相同依赖,最好统一成同一个版本,以免二者发生冲突。

2.本地依赖问题

如果依赖本地的一个aar或者jar包,在library模块中引用一般没有问题,例如下面代码,我们把aar,以及native二进制的目录armeabi,armeabi-v7a等全放在library模块/libs目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apply plugin: 'com.android.library'

android {
...
repositories {
flatDir {
dirs 'libs'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile(name: 'faceplatform-release', ext: 'aar')
...
}

这时,library模块可以正确编译,aar也生成出来了,但是引用它的app模块报错了:

1
Error:Failed to resolve: :faceplatform-release:

app模块编译也需要这个依赖库,但是找不到。这时,你可以把所有的aar复制一份到app模块里,这样编译没问题了,但是造成了很多重复文件,有个更好的办法可以不用拷贝文件,修改app模块的gradle即可:

1
2
3
4
5
6
7
8
9
10
apply plugin: 'com.android.application'

android {
...
repositories {
flatDir {
dirs '../library/libs'
}
}
}

这样的意思是去library模块/libs目录下去找找依赖吧,这样就能找到需要的本地依赖库了。

二.Butter Knife在library模块使用的问题

Butter Knife是一个View注入框架,有了它,让我少写了很多findviewById(),在app模块中使用很简单:

1
2
3
4
dependencies {
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

代码中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ExampleActivity extends Activity {
@BindView(R.id.user) EditText username;
@BindView(R.id.pass) EditText password;

@BindString(R.string.login_error) String loginErrorMessage;

@OnClick(R.id.submit) void submit() {
// TODO call server...
}

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}

但如果要在library模块中使用,不但要修改gradle,java代码也要修改如下,这些github上都有说明:
https://github.com/JakeWharton/butterknife

  • 需要使用gradle插件做一些预处理:
    插件声明
    1
    2
    3
    4
    5
    6
    7
    8
    buildscript {
    repositories {
    mavenCentral()
    }
    dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:8.8.1'
    }
    }
    在library模块中使用插件:
    1
    2
    apply plugin: 'com.android.library'
    apply plugin: 'com.jakewharton.butterknife'
  • 把R改成R2:
    1
    2
    3
    4
    5
    class ExampleActivity extends Activity {
    @BindView(R2.id.user) EditText username;
    @BindView(R2.id.pass) EditText password;
    ...
    }

三.资源ID常量的变化

在library模块中声明的资源ID,不能使用switch/case来判断,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void onClick(View src){
switch(src.getId()) {
case R.id.playbtn:
...
break;

case R.id.stopbtn:
...
break;

case R.id.btnmenu:
...
break;
}
}

上面的写法是错误的,android studio会提示错误,原因是资源ID在app模块中是声明为final的,也就是无法再改变的,而library模块中并没有声明final,只有在最终编译成apk的时候才会被统一进行确定,如果事先声明了final,多个模块编译到一个apk时难免互相有ID重复,或者与依赖库或app中的ID重复,这样就会造成错误。因此就不能采用switch/case来判断id了,而用if/else就没毛病了。

1
2
3
4
5
6
7
8
9
10
public void onClick(View src){
int id = src.getId();
if (id == R.id.playbtn){
...
} else if (id == R.id.stopbtn){
...
} else if (id == R.id.btnmenu){
...
}
}

上面butterknife的问题,也是由于这个原因,所以butterknife会用gradle插件重新生成一个资源文件R2,在R2中的ID都是用final来修饰的,所以就可以解决这个问题了。

四.接入GoogleService推送

使用google推送,或者是Firebase,就需要接入GoogleService,按照Firebase官网的指导,接入方法是先添加Gradle插件:

1
2
3
4
5
6
7
8
9
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.2'
classpath 'com.google.gms:google-services:3.1.0'
}
}

并且使用插件:

1
2
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

这个插件必须只能使用在app模块,如果使用在library模块是要报错的。但是,如果我们的Firebase接收消息的业务逻辑写在了library模块,就需要在library模块中引用firebase的依赖库,而在app模块中apply plugin,google-services.json文件也必须放到app模块下,才能保证顺利编译。

1
2
3
4
5
6
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.google.firebase:firebase-core:11.0.4'
compile 'com.google.firebase:firebase-messaging:11.0.4'
testCompile 'junit:junit:4.12'
}

五.资源覆盖问题

过去的单模块编写,同一资源目录下不允许出现两个同名的资源,但是在多模块的情况下,app模块的assets、res下的颜色,图片,字符串等都会覆盖library模块或者远程依赖带过来的同名资源,这样就可以方便的在app模块中修改library已定义好的主题,字符串等。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器