最近看了微信Android模块化架构重构实践这篇文章,刚好自己又正在搭建新项目的框架,用到组件化开发;感觉文章里面的一些技巧很有用,就跟着实现了一下,写一下自己的看法
模块间的交互
首先是解决模块之前的依赖问题,模块间肯定是不能相互依赖的,那如何进行数据交互就是一个问题了;比如用户模块和其他模块,其他模块如何在不依赖用户模块的情况下获取到用户信息;
使用EventBus
想要获取用户信息,那User类肯定是要引用的,肯定是要提取出User类放到公共模块里面,然后获取User可以通过EventBus来获取数据
公共模块将EventBus发送的Event定义为接口
public interface UserCallback {
/**
* 获取用户数据
*
* @param user
*/
void getUser(User user);
}
然后在用户模块订阅事件,返回用户信息
@Subscribe
public void getUser(UserCallback callback){
callback.getUser(new com.dhht.baselibrary.User());
}
在其他模块就可以通过EventBus来发送事件获取到用户信息
EventBus.getDefault().post(new UserCallback() {
@Override
public void getUser(User user) {
mUser = user;
}
});
但是讲道理EventBus还是少用的好,业务多了会生成很多Event类,感觉是有点难受的,而且代码阅读起来非常难;
SPI机制
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
整体机制图如下:
具体的实现(可以略过)
首先也是把User放在公共模块里面,获取用户信息的接口也放在公共模块里面
package com.dhht.baselibrary;
public interface UserService {
/**
* 获取user
*
* @return
*/
User getUser();
}
然后在用户模块里面实现接口
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
的文件,文件内容就是实现类的路径
com.dhht.user.UserImpl
这个时候再其他模块使用这个实现类就可以通过SPI机制来获取
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接口
public interface UserService extends IProvider {
UserInfo getUser();
}
然后在用户模块实现接口,并且添加@Route
注解
@Route(path = "/user/UserService")
public class UserServiceImpl implements UserService {
@Override
public UserInfo getUser() {
return new UserInfo("Tyhj");
}
@Override
public void init(Context context) {
}
}
然后在其他模块通过ARouter注解获取实例
@Autowired//(name = "/user/UserService")
UserService mUserService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.getInstance().inject(this);
...
方法比较简单,相对于正常的代码只是添加了一个注解而已,ARouter的最新版本如下,每个模块都需要添加注解插件(第二行),库(第一行)只需要在公共模块添加就好了;
//arouter
api 'com.alibaba:arouter-api:1.4.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
使用ARouter还需要在每个模块的build.gradle的defaultConfig节点下添加如下代码
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文件的实现
def includeWithApi(String moduleName) {
//先正常加载这个模块
include(moduleName)
//找到这个模块的路径
String originDir = project(moduleName).projectDir
//这个是新的路径
String targetDir = "${originDir}-api"
//新模块的名字
def sdkName = "${project(moduleName).name}-api"
//这个是公共模块的位置,我放了一个 新建的api.gradle 文件进去
String apiGradle = project(":commonlibrary").projectDir
// 每次编译删除之前的文件
deleteDir(targetDir)
//复制.api文件到新的路径
copy() {
from originDir
into targetDir
exclude '**/build/'
exclude '**/res/'
include '**/*.api'
}
//复制 gradle和AndroidManifest文件到新的路径,作为该模块的文件
copy() {
from apiGradle
into targetDir
include 'api.gradle'
include '**/AndroidManifest.xml'
}
//重命名一下gradle
def build = new File(targetDir + "/api.gradle")
if (build.exists()) {
build.renameTo(new File(targetDir + "/build.gradle"))
}
// 重命名.api文件,生成正常的.java文件
renameApiFiles(targetDir, '.api', '.java')
//正常加载新的模块
include ":$sdkName"
}
private void deleteDir(String targetDir) {
FileTree targetFiles = fileTree(targetDir)
targetFiles.exclude "*.iml"
targetFiles.each { File file ->
file.delete()
}
}
/**
* rename api files(java, kotlin...)
*/
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)))
}
}
//这些还是之前的写法
include ':app', ':commonlibrary', ':expresscheck', ':main'
//需要生成新的模块的使用这个方法
includeWithApi(":user")
其实讲的还是比较清楚了,我首先复制.api文件去生成Java文件,想要生成新的api模块,得有gradle
和AndroidManifest
文件才行,而这个api模块显然不需要过多的配置,于是我自己先生成一个简单的gradle
文件,就是其他模块复制过来的,基础配置而已,然后复制到新的api模块搞定,对于AndroidManifest文件,基础模块肯定是没有什么配置的,复制过来使用完事儿;其实复制过来的AndroidManifest里面的package路径不对,没关系反正这个文件是用不到的;
我这里创建的是Android Library,其实创建Java Library也是一样的,只是我感觉Android Library更好一点;可能感觉稍微有点复杂,其实只需要编写一个通用的setting.gradle文件然后改改.java文件名而已,这个也是微信重构的一个技巧,我觉得还是挺好的