Android项目模块化

Android项目模块化

为何模块化

  1. 减少代码耦合性,能够实现功能业务的拆分
  2. 能够实现业务的快速实现和功能的复用
  3. 单模块开发减少编译时间和实现模块代码的责任制
  4. 实现功能模块的替换和迭代

其中最重要的是模块的复用和代码的解耦。由于经常会有其他的分支项目加入,所以模块的可复用就显得非常重要。代码解耦可以保证一个业务模块的高效迭代和代码整洁,防止随着项目的推进导致代码的冲突和堆叠。

项目结构

项目结构

除去 app 壳和 core 核心库以外,所有的模块命名方式为lib+基础库,module_业务

概述

整个项目分为四层,以Core为基础的核心功能层,以libarch和各个lib组件的基础层,以module业务为主的模块层,最后是以app为中心的应用层。

核心层

Core提供了网络请求,数据库,文件缓存,下载,Utils等一系列核心功能,为上层服务架设基础设施。
基础功能

网络框架选择了 Okhttp3+Retrofit2 的方式,这样可以有一个好处,可以在Core中预先统一设置请求OkHttpClinetHeader参数和添加一些功能性Interceptor,如日志HttpLoggingInterceptor,之后再在各个模块中定义Retrofit 服务器接口,实现接口的分发。

数据库使用Google推荐的RoomRoom是一个对象关系映射(ORM)库。Room抽象了SQLite的使用,可以在充分利用SQLite的同时访问流畅的数据库。其设计原理为使用apt查找注解后再拼凑sql语句。

文件缓存为常用的SharedPreferences,抽象出AbstractSharePreferences,实现每个模块生成一个preferences

下载(暂定)使用FileDownloader

基础库

libarch属于模块基础库,提供统一的主题,样式和基础化功能

  1. 主题模块—— 主要将要重复使用的Style,Color,Dimen,Drawable等这些资源放到同一个模块下以方便其他各个子模块的调用。包括app图标,分为方形和圆形;
    App默认的style及其各个衍生版本
  • AppTheme 基础主题样式
  • AppTheme.NoActionBar 使用ToolBar的App默认样式
  • AppTheme.Immersive 沉浸式样式
  • AppTheme.Full 全屏样式 常用颜色和app默认三原色:colorPrimary,colorPrimaryDark,colorAccent;常用间距大小和字体大小
    line_height系列和text字体系列;常用短语等;
  1. 基于的 ARouter 的路由配置和c/s服务模型通信;
  2. 提供项目架构,mvp与mvvm两种模式;
  3. 通用埋点等(如友盟),时长统计(TODO)

组件库

lib组件库一般不依赖任何基础库,包括Corelibarch,它的初衷只是为了实现特定的某种功能,纯粹而又优雅。

  • liblist-通用的下拉上拉列表控件
  • libreader-图文阅读器

业务模块

module_业务是代码的主要生产区,主要以业务功能进行划分。

模块结构

我们所说的模块化一般指的就是将业务分割成多个模块,并使其能够单独运行。如上图所示,结构中包括:

  • debug文件夹包含一份单独模块运行的AndroidMainfest文件;
  • db用于保存模块内的数据库;
  • model含有业务实体类bean和网络请求接口http;
  • service 包含用于c/s通信的service实现类;
  • singleton 是单模块运行时需要的帮助类,一般包括一个Application和入口MainActivity;
  • ui 业务界面与逻辑
  • utils 常用帮助类
    可以看到整个模块结构实际上与普通的项目结构很类似。

app在模块化项目中担任的职责就弱了很多,它只是作为一个壳承担程序入口的作用。

如何实现多模块

单模块

我们使用 Gradle 控制一个模块是否是单模块。
一个模块能否单独运行需要满足以下几个条件:

  1. gradle中需要 apply plugin:’com.android.application’
  2. 需要一个application和一个默认入口Activity

所有的条件都可以在 gradle 中控制
config.gradle 统一控制

1
2
3
4
5
6
7
modules = [
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
19
def 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
2
3
4
5
6
7
// libarch 的 provider 包中
interface AppService : IProvider {

fun initNimRedDot()

fun detailNavigation();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//模块中的实现类
@Route(path = "/app/main", name = "APP主框架")
class AppServiceImpl :AppService {
override fun detailNavigation() {

}

override fun initNimRedDot() {

}

override fun init(context: Context?) {
}
}

单模块间数据共享

当我们在单模块运行时有时候会需要其他模块的数据,比如用户的登录信息等,这时候就需要能够进行跨进程传数据的方法,即IPC方式。
常见的IPC方法有:文件,Intent传递,ContentProviderSharedPreferences,MessagersSocketaidl等几种方法。由于我们要自定义方法所以最佳选择为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
56
class 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
32
object 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

@Synchronized
fun getUserToken(context: Context): String? {
val cr = context.contentResolver
val uri = Uri.parse(AUTH_MODULE_AUTHORITY + "token")
return cr.getType(uri)
}

@Synchronized
fun getUserAccId(context: Context): String? {
val cr = context.contentResolver
val uri = Uri.parse(AUTH_MODULE_AUTHORITY + "accId")
return cr.getType(uri)
}

@Synchronized
fun getNimToken(context: Context): String? {
val cr = context.contentResolver
val uri = Uri.parse(AUTH_MODULE_AUTHORITY + "nimToken")
return cr.getType(uri)
}

}

有些手机在应用未开启时通过ContentProvider跨应用访问数据若是没有自启动权限则会导致访问失败

路由与通信

整个多模块架构中最重要的角色“路由”和“通信”都是使用ARouter进行担任。如果不使用ARouter框架的话也可以简单实现路由和通信的功能。

  1. 路由:在模块初始化的时候将需要进行路由配置的Activity保存到一个路由的路由映射表中,统一管理,统一查找。也可以参考Retrofit方式,直接定义跳转Java接口,如果需要传递额外参数,则以函数参数的方式定义。这个Java接口是没有实现类的,可以使用动态代理方式。
  2. 通信:也是采取先注册的方式获取声明的Class类,之后可以通过反射生成类来获取对象。

除去运行时注册的这种方法外,还有两种方式,一种是采用APT通过编译时期对注解的处理生成处理类;
另外一种则是通过Gradle Transform,这是Android Gradle编译提供的一个接口,可以供开发自定义一些功能,而我们就可以根据这个功能生成映射匹配,这种方式和APT类似,APT是运行在代码编译时期,而且Transform是直接扫描class,然后再生成新的class,class中包含Map映射信息。修改class文件,使用的是javassist一个第三方库。

总结

模块化架构主要思路就是分而治之,把依赖整理清楚,减少代码冗余和耦合,在把代码抽取到各自的模块后,了解各个模块的通信方式,以及可能发生的问题,规避问题或者解决问题。最后为了开发和调试方便,开发一些周边工具,帮助开发更好的完成任务。

文章作者: cpacm
文章链接: http://www.cpacm.net/2019/11/06/Android进阶之路——模块化实践/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 cpacm
打赏
  • 微信
  • 支付宝

评论