原文链接:https://www.jianshu.com/p/902df67e9728
最近看了微信Android模块化架构重构实践这篇文章,刚好自己又正在搭建新项目的框架,用到组件化开发;感觉文章里面的一些技巧很有用,就跟着实现了一下,写一下自己的看法
模块间的交互
首先是解决模块之前的依赖问题,模块间肯定是不能相互依赖的,那如何进行数据交互就是一个问题了;比如用户模块和其他模块,其他模块如何在不依赖用户模块的情况下获取到用户信息;
使用EventBus
想要获取用户信息,那User类肯定是要引用的,肯定是要提取出User类放到公共模块里面,然后获取User可以通过EventBus来获取数据
公共模块将EventBus发送的Event定义为接口
1 2 3 4 5 6 7 8 9
| public interface UserCallback {
void getUser(User user); }
|
然后在用户模块订阅事件,返回用户信息
1 2 3 4
| @Subscribe public void getUser(UserCallback callback){ callback.getUser(new com.dhht.baselibrary.User()); }
|
在其他模块就可以通过EventBus来发送事件获取到用户信息
1 2 3 4 5 6
| EventBus.getDefault().post(new UserCallback() { @Override public void getUser(User user) { mUser = user; } });
|
但是讲道理EventBus还是少用的好,业务多了会生成很多Event类,感觉是有点难受的,而且代码阅读起来非常难;
SPI机制
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
整体机制图如下:

具体的实现(可以略过)
首先也是把User放在公共模块里面,获取用户信息的接口也放在公共模块里面
1 2 3 4 5 6 7 8 9 10
| package com.dhht.baselibrary; public interface UserService {
User getUser(); }
|
然后在用户模块里面实现接口
1 2 3 4 5 6 7 8
| package com.dhht.user;
public class UserImpl implements UserService { @Override public User getUser() { return new User("UserImpl"); } }
|
需要在user/src/main/resources/META-INF.services/
目录下面新建文件名为com.dhht.baselibrary.UserService
的文件,文件内容就是实现类的路径
这个时候再其他模块使用这个实现类就可以通过SPI机制来获取
1 2 3 4 5 6
| ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class); Iterator<UserService> iterator = userServices.iterator(); while (iterator.hasNext()) { UserService userService = iterator.next(); ToastUtil.showShort(userService.getUser().getName()); }
|
ARouter
上面的过程稍微有点复杂,也没必要去实现,这个是一种思想,很多路由框架都是借助了这种思想,而且使用非常方便,比如阿里的ARouter框架;用户类不变,接口需要实现IProvider接口
1 2 3
| public interface UserService extends IProvider { UserInfo getUser(); }
|
然后在用户模块实现接口,并且添加@Route
注解
1 2 3 4 5 6 7 8 9 10 11 12
| @Route(path = "/user/UserService") public class UserServiceImpl implements UserService { @Override public UserInfo getUser() { return new UserInfo("Tyhj"); }
@Override public void init(Context context) {
} }
|
然后在其他模块通过ARouter注解获取实例
1 2 3 4 5 6 7 8
| @Autowired UserService mUserService;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ARouter.getInstance().inject(this); ...
|
方法比较简单,相对于正常的代码只是添加了一个注解而已,ARouter的最新版本如下,每个模块都需要添加注解插件(第二行),库(第一行)只需要在公共模块添加就好了;
1 2 3
| //arouter api 'com.alibaba:arouter-api:1.4.1' annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
|
使用ARouter还需要在每个模块的build.gradle的defaultConfig节点下添加如下代码
1 2 3 4 5
| javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] } }
|
提取出api模块
如果每次有一个模块要使用另一个模块的接口都把接口和相关文件放到公共模块里面,那么公共模块会越来越大,而且每个模块都依赖了公共模块,都依赖了一大堆可能不需要的东西;
所以我们可以提取出每个模块提供api的文件放到各种单独的模块里面;比如user模块,我们把公共模块里面的User和UserInfoService放到新的user-api模块里面,这样其他模块使用的时候可以单独依赖于这个专门提供接口的模块,以此解决公共模块膨胀的问题
自动生成Library
为了写代码方便,我们可以在写代码的时候,每个模块的东西都写在一起,比如User提供的接口我们也正常写在用户模块里面,在编译的时候,再使用gradle来自动生成各个api模块,这样会方便很多
原理是这样的,我们把需要单独生成api模块的.java文件改为另一种文件类型比如把UserInfo.java改为UserInfo.api,在设置/Editor/File Type中找到Java类型,添加*.api,然后就可以和Java文件一样使用了;
在项目的setting.gradle文件里面添加方法includeWithApi("module名字")
,用这个方法来代替include ":module名字"
,这个方法会从这个module里面找到以.api结尾的文件,复制到新的module里面并重命名,当然也会复制gradle
文件和AndroidManifest
文件,以此生成新的api模块
具体实现
setting.gradle文件的实现
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
| def includeWithApi(String moduleName) { include(moduleName) String originDir = project(moduleName).projectDir String targetDir = "${originDir}-api" String originName=project(moduleName).name; def sdkName = "${originName}-api"
String apiGradle = project(":baselibrary").projectDir
deleteDir(targetDir)
copy() { from originDir into targetDir exclude '**/build/' exclude '**/res/' include '**/*.api' }
copy() { from "${apiGradle}/src/main/AndroidManifest.xml" into "${targetDir}/src/main/" }
copy() { from "${apiGradle}/api.gradle" into "${targetDir}/" }
deleteEmptyDir(new File(targetDir))
String packagePath = "${targetDir}/src/main/java/com/dhht/${originName}/api";
fileReader("${targetDir}/src/main/AndroidManifest.xml", "commonlibrary","${originName}.api");
new File(packagePath).mkdirs()
def build = new File(targetDir + "/api.gradle") if (build.exists()) { build.renameTo(new File(targetDir + "/build.gradle")) }
renameApiFiles(targetDir, '.api', '.java')
include ":$sdkName" }
private void deleteEmptyDir(File dir) { if (dir.isDirectory()) { File[] fs = dir.listFiles(); if (fs != null && fs.length > 0) { for (int i = 0; i < fs.length; i++) { File tmpFile = fs[i]; if (tmpFile.isDirectory()) { deleteEmptyDir(tmpFile); } if (tmpFile.isDirectory() && tmpFile.listFiles().length <= 0) { tmpFile.delete(); } } } if (dir.isDirectory() && dir.listFiles().length == 0) { dir.delete(); } } }
private void deleteDir(String targetDir) { FileTree targetFiles = fileTree(targetDir) targetFiles.exclude "*.iml" targetFiles.each { File file -> file.delete() } }
private def renameApiFiles(root_dir, String suffix, String replace) { FileTree files = fileTree(root_dir).include("**/*$suffix") files.each { File file -> file.renameTo(new File(file.absolutePath.replace(suffix, replace))) } }
def fileReader(path, name,sdkName) { def readerString = ""; def hasReplace = false
file(path).withReader('UTF-8') { reader -> reader.eachLine { if (it.find(name)) { it = it.replace(name, sdkName) hasReplace = true } readerString <<= it readerString << '\n' }
if (hasReplace) { file(path).withWriter('UTF-8') { within -> within.append(readerString) } } return readerString } }
include ':app', ':baselibrary' includeWithApi ":user" includeWithApi ":other"
|
其实讲的还是比较清楚了,我首先复制.api文件去生成Java文件,想要生成新的api模块,得有gradle
和AndroidManifest
文件才行,而这个api模块显然不需要过多的配置,于是我自己先生成一个简单的gradle
文件,就是其他模块复制过来的,基础配置而已,然后复制到新的api模块搞定,对于AndroidManifest文件,基础模块肯定是没有什么配置的,复制过来使用完事儿;
AndroidManifest路径问题
下面这个demo是随便写的,不是按照组件化来写的,只是简单展示一下这个脚本的作用而已,组件化框架搭建点这里,第一个版本写完后能运行没发现问题,但是有位兄弟发现build的时候居然失败了,报错如下:
1
| AGPBI: {"kind":"error","text":"Program type already present: com.dhht.commonlibrary.BuildConfig","sources":[{}],"tool":"D8"}
|
这个错误很常见,意思就是com.dhht.commonlibrary.BuildConfig
这个文件重复了,明显是因为我直接拷贝AndroidManifest文件,里面的包名没有修改导致的
1 2
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.dhht.commonlibrary" >
|
发现只要将minSdkVersion
设置为21
就可以避免这个问题
1 2 3
| defaultConfig { minSdkVersion 21 ...
|
但是后来打包签名apk的又报错了,那其实我们随便设置不同的包名就可以了,但是包名不能设置不存在的路径,所以在新的模块的原包下新建一个api文件夹,然后在复制过来的AndroidManifest里面修改包名,也不用把minSdkVersion
设置为21,当然都是脚本完成
1 2
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.dhht.user.api">
|
我这里创建的是Android Library,其实创建Java Library也是一样的,只是我感觉Android Library更好一点;可能感觉稍微有点复杂,其实只需要编写一个通用的setting.gradle文件然后改改.java文件名而已,这个也是微信重构的一个技巧,我觉得还是挺好的
项目地址:https://github.com/tyhjh/module_api