Android项目模块化
为何模块化
- 减少代码耦合性,能够实现功能业务的拆分
- 能够实现业务的快速实现和功能的复用
- 单模块开发减少编译时间和实现模块代码的责任制
- 实现功能模块的替换和迭代
其中最重要的是模块的复用和代码的解耦。由于经常会有其他的分支项目加入,所以模块的可复用就显得非常重要。代码解耦可以保证一个业务模块的高效迭代和代码整洁,防止随着项目的推进导致代码的冲突和堆叠。
项目结构
除去 app 壳和 core 核心库以外,所有的模块命名方式为lib+基础库,module_业务
概述
整个项目分为四层,以Core为基础的核心功能层,以libarch和各个lib组件的基础层,以module业务为主的模块层,最后是以app为中心的应用层。
核心层
Core提供了网络请求,数据库,文件缓存,下载,Utils等一系列核心功能,为上层服务架设基础设施。
网络框架选择了 Okhttp3+Retrofit2
的方式,这样可以有一个好处,可以在Core中预先统一设置请求OkHttpClinet
的Header
参数和添加一些功能性Interceptor,如日志HttpLoggingInterceptor
,之后再在各个模块中定义Retrofit 服务器接口,实现接口的分发。
数据库使用Google推荐的Room
,Room
是一个对象关系映射(ORM)库。Room
抽象了SQLite
的使用,可以在充分利用SQLite
的同时访问流畅的数据库。其设计原理为使用apt查找注解后再拼凑sql语句。
文件缓存为常用的SharedPreferences
,抽象出AbstractSharePreferences
,实现每个模块生成一个preferences
下载(暂定)使用FileDownloader
基础库
libarch
属于模块基础库,提供统一的主题,样式和基础化功能
- 主题模块—— 主要将要重复使用的
Style,Color,Dimen,Drawable
等这些资源放到同一个模块下以方便其他各个子模块的调用。包括app图标,分为方形和圆形;
App默认的style及其各个衍生版本
- AppTheme 基础主题样式
- AppTheme.NoActionBar 使用ToolBar的App默认样式
- AppTheme.Immersive 沉浸式样式
- AppTheme.Full 全屏样式 常用颜色和app默认三原色:colorPrimary,colorPrimaryDark,colorAccent;常用间距大小和字体大小
line_height系列和text字体系列;常用短语等;
- 基于的
ARouter
的路由配置和c/s服务模型通信; - 提供项目架构,mvp与mvvm两种模式;
- 通用埋点等(如友盟),时长统计(TODO)
组件库
lib组件库一般不依赖任何基础库,包括Core
和libarch
,它的初衷只是为了实现特定的某种功能,纯粹而又优雅。
liblist
-通用的下拉上拉列表控件libreader
-图文阅读器
业务模块
module_业务是代码的主要生产区,主要以业务功能进行划分。
我们所说的模块化一般指的就是将业务分割成多个模块,并使其能够单独运行。如上图所示,结构中包括:
- debug文件夹包含一份单独模块运行的
AndroidMainfest
文件; - db用于保存模块内的数据库;
- model含有业务实体类bean和网络请求接口http;
- service 包含用于c/s通信的service实现类;
- singleton 是单模块运行时需要的帮助类,一般包括一个
Application
和入口MainActivity
; - ui 业务界面与逻辑
- utils 常用帮助类
可以看到整个模块结构实际上与普通的项目结构很类似。
壳
app在模块化项目中担任的职责就弱了很多,它只是作为一个壳承担程序入口的作用。
如何实现多模块
单模块
我们使用 Gradle 控制一个模块是否是单模块。
一个模块能否单独运行需要满足以下几个条件:
- gradle中需要 apply plugin:’com.android.application’
- 需要一个application和一个默认入口Activity
所有的条件都可以在 gradle 中控制
在 config.gradle
统一控制1
2
3
4
5
6
7modules = [
isAuthModule : true,
isReaderModule : true,
isNimModule : true,
isZhumuModule : true,
isServiceModule: true,
]
根据条件判断1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19def isModule = rootProject.ext.modules.isAuthModule
if (isModule) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
sourceSets {
main {
if (!isModule) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
之后就需要在 singleton 文件夹里加入单独运行时需要的application和入口Activity
模块间跳转
使用阿里开源的ARouter进行路由配置和跳转1
ARouter.getInstance().build(ARouterUtils.AROUTER_USER_TASK).navigation()
这里我将所有的路由的路径放在统一一个路由表中ARouterUtils
方便管理和修改
模块间通信
利用 ARouter 的 IProvider作为c/s通信的基础框架。
通常我会将 Service 服务接口下沉到 libarch 中,方便所有模块能够方便找到。而实现类 ServiceImpl 则是放入各个模块之中,对外提供服务功能。
1 | // libarch 的 provider 包中 |
1 | //模块中的实现类 |
单模块间数据共享
当我们在单模块运行时有时候会需要其他模块的数据,比如用户的登录信息等,这时候就需要能够进行跨进程传数据的方法,即IPC方式。
常见的IPC方法有:文件,Intent
传递,ContentProvider
,SharedPreferences
,Messagers
,Socket
和aidl
等几种方法。由于我们要自定义方法所以最佳选择为aidl
,次级为ContentProvider
。我选择的是通过ContentPriovider。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56class AccountContentProvider : ContentProvider() {
private val AUTHORITY = "com.dracom.android.accountprovider"
val mMatcher: UriMatcher;
init {
mMatcher = UriMatcher(UriMatcher.NO_MATCH);
// 初始化
mMatcher.addURI(AUTHORITY, "token", 1)
mMatcher.addURI(AUTHORITY, "accId", 2)
mMatcher.addURI(AUTHORITY, "nimToken", 3)
mMatcher.addURI(AUTHORITY, "phone", 4)
// 若URI资源路径 = content://cn.scu.myprovider/user ,1
}
override fun onCreate(): Boolean {
return true
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
val userInfo = UserSharedPreferences.getInstance(context).userInfo
when (mMatcher.match(uri)) {
1 -> return userInfo.loginToken
2 -> return userInfo.accId
3 -> return userInfo.token
4 -> return userInfo.phoneNumber
else -> return ""
}
}
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
if (mMatcher.match(uri) == 1) {
val userToken = UserSharedPreferences.getInstance(context).userInfo.loginToken
val cursor = MatrixCursor(arrayOf(CURSOR_COLUMN_NAME, CURSOR_COLUMN_VALUE))
cursor.addRow(arrayListOf("user_token", userToken))
return cursor
}
return null
}
}
跨进程数据读取辅助类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32object MultiProcessSpUtils {
val CURSOR_COLUMN_NAME = "cursor_name"
val CURSOR_COLUMN_VALUE = "cursor_value"
val AUTH_MODULE_AUTHORITY = "content://com.dracom.android.accountprovider/"
var userToken: String? = null
fun getUserToken(context: Context): String? {
val cr = context.contentResolver
val uri = Uri.parse(AUTH_MODULE_AUTHORITY + "token")
return cr.getType(uri)
}
fun getUserAccId(context: Context): String? {
val cr = context.contentResolver
val uri = Uri.parse(AUTH_MODULE_AUTHORITY + "accId")
return cr.getType(uri)
}
fun getNimToken(context: Context): String? {
val cr = context.contentResolver
val uri = Uri.parse(AUTH_MODULE_AUTHORITY + "nimToken")
return cr.getType(uri)
}
}
有些手机在应用未开启时通过
ContentProvider
跨应用访问数据若是没有自启动权限则会导致访问失败
路由与通信
整个多模块架构中最重要的角色“路由”和“通信”都是使用ARouter进行担任。如果不使用ARouter
框架的话也可以简单实现路由和通信的功能。
- 路由:在模块初始化的时候将需要进行路由配置的Activity保存到一个路由
的路由映射表中,统一管理,统一查找。也可以参考Retrofit方式,直接定义跳转Java接口,如果需要传递额外参数,则以函数参数的方式定义。这个Java接口是没有实现类的,可以使用动态代理方式。 - 通信:也是采取先注册的方式获取声明的Class类,之后可以通过反射生成类来获取对象。
除去运行时注册的这种方法外,还有两种方式,一种是采用APT通过编译时期对注解的处理生成处理类;
另外一种则是通过Gradle Transform,这是Android Gradle编译提供的一个接口,可以供开发自定义一些功能,而我们就可以根据这个功能生成映射匹配,这种方式和APT类似,APT是运行在代码编译时期,而且Transform是直接扫描class,然后再生成新的class,class中包含Map映射信息。修改class文件,使用的是javassist一个第三方库。
总结
模块化架构主要思路就是分而治之,把依赖整理清楚,减少代码冗余和耦合,在把代码抽取到各自的模块后,了解各个模块的通信方式,以及可能发生的问题,规避问题或者解决问题。最后为了开发和调试方便,开发一些周边工具,帮助开发更好的完成任务。