Android组件化开发+自定义路由框架
技术背景
独立开发,集成打包
让App内各个功能模块能够独立开发单元测试,也可以所有模块集成打包,统一测试
独立开发
更改build.gradle
的配置,使得每个功能模块都成为application
,可以独立打包成apk,单独运行。单个模块,独立测试。
集成打包
更改build.gradle
的配置,使得原先每个单独模块,都变成library,被 主模块引用,这时候只有主模块能够打包apk,所有功能都集成在这个apk内。
实现功能模块的整体移植,灵活拔插
故事背景:当公司有多个安卓开发人员,开发出核心业务相同,但是UI不同,其他业务不同的一系列App时(如果核心业务是X,你们有5个开发人员,做出了A,B,C,D,E 5个app,都包含核心业务X,但是除了X之外,其他的业务模块各不相同),如果领导要把A里面的一个非核心功能,挪到B里面...
现状:开发B的程序猿可能要骂娘,因为他在从移植A的代码中剥离代码遇到了很多高耦合,低内聚的类结构,挪过来之后,牵一发而动全身,动一点小地方,整个代码满江红。
理想:如果这个时候,我们通过代码框架的配置,能够把A里面的一个模块,作为一个module 移植到工程内部,然后主module 来引用这个module,略微写一些代码来使得这个功能模块在app中生效。那么无论是多少个功能模块,都可以作为整体来给其他app复用。这样开发人员也不用相互骂娘了,如果挪过来的模块存在bug或者其他问题,也不用甩锅,模块原本是谁开发的,找谁就好了。
保证App内业务模块的相互隔离,但是又不妨碍业务模块之间的数据交互
我们开发app的功能模块,一个业务,可能是通过一个Activity或者一个Fragment 作为对外的窗口,也可能是所谓窗口,就是这个业务,相对于其他模块,"有且只有"一个入口,没有任何其他可以触达到这个业务的途径。业务代码之间相互隔离,绝对不可以有相互引用。那么,既然相互不会引用,那A模块一定要用到B模块的数据,怎么办呢?下文提供解决方案。
代码结构现状以及理想状态一览
外壳层:app module
内部代码只写 app的骨骼框架,比如APP是选项卡结构,下方有N个TAB,通过Fragment来进行切换模块。此时,外壳层app module
,就只需要写上这种UI架构的框架代码即可,至于有多少个模块,需要代码去读取配置进行显示。
业务层
我们的业务模块,对外接口可能是一个Activity (比如说,登录模块,只对外提供一个LoginActivity,有且仅有这一个窗口)或者 是一个Fragment,如果app的UI框架是通过切换Fragment来却换业务模块的话。用business这个目录,将所有的业务模块包含进去,每个模块又是独立的module,这样既实现了业务代码隔离,又能一眼看到所有的业务模块,正所谓,一目了然。
功能组件层
每一个业务模块,不可避免的需要用到一些公用工具类,有的是第三方SDK的再次封装,有的是自己的工具类,或者自己写的自定义控件,还有可能是所有业务模块都需要的辅助模块,都放在这里。
路由框架层
设计这一层,是想让app内的所有Activity,业务模块Fragment,以及模块之间的数据交互,都由这一层开放出去的接口来负责。
gradle统一配置文件
工程内部的一些全局gradle变量,放在这里,整个工程都有效。
module编译设置
setting.gradle
配置要编译的module,也可以做更复杂的操作,比如,写gradle代码去自动生成一些module,免除人为创建的麻烦。
功能组件化的实现思路,实现组件移植拔插
gradle.properties
能够兼顾每个模块的单独开发,单独测试和整体打包、统一测试。其实就一个核心:Gradle编程
打开gradle.properties
文件:
#true: 集成模式运行 false: 组件模式运行
isModule=false
通过一个全局变量,就可以控制当前是要 模块化单元测试呢?还是要集成打包apk测试。
那么,只写一个isModule就完事了吗?当然不是,我们要使用这个全局变量。
app外壳层module的build.gradle
注意:写在dependencies
if (isModule.toBoolean()) {
implementation project(":business:activity_XXX")
//...在这里引用更多业务模块
}
每个业务module的build.gradle
第一处:判定isModule
,决定当前module是要当成library还是application
if (isModule.toBoolean()) {
apply plugin:'com.android.library'
} else {
apply plugin:'com.android.application'
}
第二处:更改defaultConfig
里面的部分代码,为什么要改?因为当当前module作为library的时候,不能有applicationId "xxx"
这一句
defaultConfig {
if (!isModule.toBoolean()) {
applicationId "cn.appblog.XXXX"*
}
....
}
第三处:当业务模块module作为library的时候,不可以在AndroidManifest.xml
中写Launcher Activity
,否则,打包app module的时候,安装完毕,手机桌面上将会出现不止一个icon。而当业务模块module 作为application单独运行的时候,必须有一个Launcher Activity
。所以这里针对manifest文件进行区分对待:
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
由于要区分对待,我们就需要另外创建一个manifest文件,移除launcher配置即可。参考下图:
如何进行功能拔插
1)在app的build.gradle
里面把引用该模块的配置去掉
2)setting.gradle
的include去掉该模块
3)app module
里面,改动代码,删除UI入口,不再使用这个模块
功能的插入,同理,上面的过程倒过来走一遍
参考ARouter源码,写出自己的Router框架,统一通过Router来进行模块的切换以及组件之间数据的交互
说到路由框架的使用价值,两点:
1、在app实现组件化之后,组件之间由于代码隔离,不允许相互引用,导致相互不能直接沟通,那么,就需要一个"中间人角色"帮忙"带话"
2、app内部,不可避免地要进行Activity跳转,Fragment切换。把这些重复性的代码,都统一让路由来做吧,省了不少代码行数
阅读了阿里巴巴ARouter的源码,参照阿里大神的主要思路,简化了一些流程,去掉了一些不需要的功能,增加了一些独有的功能,加入了一些自己的想法,写出了自己的 ZRouter 路由框架。
基础知识
-
Java反射机制(路由框架里大量地使用了class反射创建对象)
-
APT 注解,注解解析机制(注解解析机制贯穿了整个路由框架)
-
javapoet,Java类的元素结构(通过自动生成代码)
如何使用
(1)在app module的自定义Application类里面,进行初始化,ZRouter准备就绪
public class FTApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ZRouter.getInstance().initRegister(this);
}
}
(2)就绪之后才可以直接使用(RouterPathConst 里面都是自己定义的String常量):
- 切换Fragment
ZRouter.getInstance().build(RouterPathConst.PATH_FRAGMENT_MINE).navigation();
- 跳转Activity
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
- 组件之间的通信,取得Mine模块的
accountNo
然后 toast出来
String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo();
Toast.makeText(getActivity(), accountNo, Toast.LENGTH_LONG).show();
如我们之前所设想的,切换Fragment,跳转Activity,组件之间的通信全部只能通过ZRouter框架来执行。
(3)退出app时,要释放ARouer的资源(主要是静态变量)
ZRouter.getInstance().release();
(4)每个业务模块,在将要暴露出去的Fragment或者Activity上,要加上注解
@ZRoute(RouterPathConst.PATH_ACTIVITY_CHART) //注册Activity
public class ChartActivity extends AppCompatActivity {···}
或者
@ZRoute(RouterPathConst.PATH_FRAGMENT_HOME) //注册Fragment
public class HomeFragment extends Fragment {···}
或者
@ZRoute(RouterPathConst.PATH_PROVIDER_MINE) // 注册数据接口
public class MineServiceImpl implements MineOpenServiceApi {···}
设计思路
干货:看源码要思路清晰,目的明确。一切技术的价值都只有一个,那就是解决实际问题。既然是解决实际问题,那我们就从这个SDK暴露出来的最外围接口为起点,看这个接口的作用是什么,解决了什么问题,顺藤摸瓜,找找它解决问题的核心方法,至于顺藤摸瓜道路上遇到的枝枝脉脉,要分清哪些是辅助类(每个人写辅助类的习惯可能都不同,所以不必太在意),哪些是核心类(核心思想一般都是大同小异)。找到了核心思想,再从头重新过几遍,SDK的设计思路就会了然于胸。
UI跳转
HomeFragment.java的54行, 这里要进行Activity跳转。
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
这里有getInstance()
方法,build()
方法,还有navigation()
方法,一个一个看
getInstance()
是处在ZRouter类内部,是ZRouter的单例模式的get方法build()
方法也是在ZRouter类内部,逻辑很简单,就是new Postcard(path)
,参数path是一个string,方法返回值是一个Postcard对象navigation()
方法是在Postcard类内部,但是,具体的执行逻辑,依然是在ZRouter类里面
getInstance()
和build()
方法都很简单,不需要花太多精力。下面继续跟随ZRouter的navigation()
方法“追查”
ZRouter 的navigation()
方法内容如下:
Object navigation(Postcard postcard) {
LogisticsCenter.complete(postcard);
switch (postcard.getRouteType()) {
case ACTIVITY: //如果是Activity,那就跳吧
return startActivity(postcard);
case FRAGMENT: //如果是Fragment,那就切换吧
return switchFragment(postcard);
case PROVIDER: //如果是Provider,那就执行业务逻辑
return postcard.getProvider(); //那就直接返回provider对象
default:
break;
}
return null;
}
发现一个可疑的代码:LogisticsCenter.complete(postcard);
看方法名,应该是对postcard对象进行完善,进去追查
/**
* Postcard字段补全
*
* @param postcard
*/
public static void complete(Postcard postcard) {
if (null == postcard) {
throw new RuntimeException("err:postcard 是空的,怎么搞的?");
}
RouteMeta routeMeta = Warehouse.routeMap.get(postcard.getPath());
if (null == routeMeta) {//如果路由meta是空,说明可能这个路由没注册,也有可能路由表没有去加载到内存中
throw new RuntimeException("err:路由寻址失败,请检查是否path写错了");
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setRouteType(routeMeta.getRouteType());
···
}
}
这段代码,从一个map
中,用path
作为key
,get
出了一个RouteMeta
对象,然后用这个对象的字段值,对参数postcard
的属性进行赋值。
刚才的navigation()
方法这里存在switch
分支,分支涉及到ACTIVITY,FRAGMENT,PROVIDER
,由于我们这次追查的只是Activity相关,所以,忽略掉其他分支,只追查startActivity(postcard);
下面是该方法的代码:
private Object startActivity(Postcard postcard) {
Class<?> cls = postcard.getDestination();
if (cls == null) {
if (cls == null)
throw new RuntimeException("没找到对应的activity,请检查路由寻址标识是否写错");
}
final Intent intent = new Intent(mContext, cls);
if (Postcard.FLAG_DEFAULT != postcard.getFlag()) {//如果不是初始值,也就是说,flag值被更改过,那就用更改后的值
intent.setFlags(postcard.getFlag());
} else {//如果沒有设定启动模式,即 flag值没有被更改,就用常规模式启动
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//常规模式启动Activity
}
//跳转只能在主线程中进行
runInMainThread(new Runnable() {
@Override
public void run() {
mContext.startActivity(intent);
}
});
return null;
}
这里只是一个简单的跳转操作,但是,发现了一个关键点,跳转的目的地class
是来自postcard
的destination
。发现规律了,原来刚才在LogisticsCenter.complete(postcard);
里面进行postcard
完善的时候,set
进去的destination
原来在这里被使用到。
那么问题的关键点就发生了转移, 这个destination class
是从map
里面get
出来的,那么,又是什么时候被put
进去的呢?
开始追踪这个map
: Warehouse.routeMap
,通过代码追踪,可以发现,唯一可能往map
里面put
东西的代码只有这一句:
/**
* 反射执行APT注册文件的注册方法
*/
private static void registerComm() {
try {
Set<String> classNames = ClassUtils.getFileNameByPackageName(mContext, RouterConst.GENERATION_PACKAGE_NAME);//找到包名下的所有class
for (String className : classNames) {
Class<?> clz = Class.forName(className);
if (IRouterZ.class.isAssignableFrom(clz)) {
IRouterZ iRouterComm = (IRouterZ) clz.getConstructor().newInstance();
iRouterComm.onLoad(Warehouse.routeMap);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
Warehouse.traversalCommMap();
}
}
利用Java反射机制,反射创建类的实例,然后执行onLoad
方法,参数正是这个map
目前为止的结论:通过追踪ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
,我们一路上遭遇了这些类或接口:
核心类
ZRouter(提供Activity跳转的接口)
辅助类或接口
Postcard ("明信片",封装我们要执行操作,这次的操作是 跳Activity)
RouteMeta ("路由参数",Postcard的基类)
RouteType ("路由类型",我们要执行的操作,用枚举来进行区分)
LogisticsCenter ("物流中心",主要封装ZRouter类的一些特殊逻辑,比如对Postcard对象进行完善补充)
Warehouse ("货舱",用hashMap来存储"路由"对象)
IRouterZ ("路由注册"接口类 ,用于反射创建对象,从而进行路由的注册)
路由框架必然有3个部分,注解定义,注解解析,以及路由对外接口。框架把这3个部分定义成了3个module,其中,每个部分的核心代码是:
zrouter-annotation
模块的@interface ZRoute
,interface IRouterZ
接口zrouter-api
模块的ZRouter
类zrouter-compiler
模块的RouterProcessor
类
数据共享
如何使用路由进行Activity跳转已经明确。那么Fragment的切换,是自定义的方法,可能有点粗糙,但是也是通俗易懂,就点到为止。但是,我们组件化的思想,就是要隔离所有的业务模块,彼此之间不能进行直接通信,如果A模块一定要使用B模块的一些数据,通过路由框架也能实现。
HomeFragment类的第72行代码:
String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo();
这句代码的意义是:在Home模块中,通过路由框架,调用Mine模块对外开放的接口accountNo();
追踪这句代码的navigation()
方法,找到真正的执行逻辑 ZRouter类
public <T> T navigation(String serviceName) {
Postcard postcard = LogisticsCenter.buildProvider(serviceName);
if (null == postcard)
return null;
LogisticsCenter.complete(postcard);//补全postcard字段值
return (T) postcard.getProvider();
}
这里:最终返回了一个Provider对象,LogisticsCenter类又有了戏份:LogisticsCenter.buildProvider(serviceName);
和LogisticsCenter.complete(postcard);
分别点进去看:
public static Postcard buildProvider(String name) {
RouteMeta routeMeta = Warehouse.routeMap.get(name);
if (null == routeMeta) {
return null;
} else {
return new Postcard(routeMeta.getPath());
}
}
buildProvider(String)
方法,其实就是从map
中找出RouteMeta
对象,然后返回一个Postcard
/**
* Postcard字段补全
*
* @param postcard
*/
public static void complete(Postcard postcard) {
if (null == postcard) {
throw new RuntimeException("err:postcard 是空的,怎么搞的?");
}
RouteMeta routeMeta = Warehouse.routeMap.get(postcard.getPath());//
if (null == routeMeta) {//如果路由meta是空,说明可能这个路由没注册,也有可能路由表没有去加载到内存中
throw new RuntimeException("err:路由寻址失败,请检查是否path写错了");
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setRouteType(routeMeta.getRouteType());
switch (routeMeta.getRouteType()) {
case PROVIDER://如果是数据接口Provider的话
Class<? extends IProvider> clz = (Class<? extends IProvider>) routeMeta.getDestination();
//从map中找找看
IProvider provider = Warehouse.providerMap.get(clz);
//如果没找到
if (null == provider) {
//执行反射方法创建,并且存入到map
try {
provider = clz.getConstructor().newInstance();
provider.init(mContext);
Warehouse.providerMap.put(clz, provider);
} catch (Exception e) {
e.printStackTrace();
}
}
postcard.setProvider(provider);
break;
default:
break;
}
}
}
complete(Postcard)
方法,其实就是完善postcard
的字段,且针对Provider
进行特别处理,反射创建Provider
对象,并建立Provider
的缓存机制,防止多次进行数据交互时进行无意义的反射创建对象。
看到这里,整个路由框架,包括模块间的通信,即讲解完毕。
做个结论:
使用路由框架的目的,是在项目代码组件化的背景之下,优化Activity跳转,Fragment切换的重复代码的编写,而统一使用路由框架的对外接口执行跳转或者切换。同时,通过路由框架的对外接口,实现组件之间的无障碍通信,保证组件的独立性。
看了阿里巴巴的ARtouer框架之后得到启发,按照它的思路来写自己的路由框架,核心思想是:APT 注解 + 反射 + 自动生成代码
使用组件api化,在模块很多的情况下优化公共模块的结构
组件API化技术
背景:这里的功能组件层 function,是存放各个业务模块都需要的公共类或者接口。这里说的公共类,也包含了刚才所提及的业务模块之间进行通信所需要的接口。举例说明:A模块,需要调用B模块的test()接口,由于A不能直接引用B模块,那这个test接口,只能放在function这个公共模块内,然后A,B同时引用,B对test接口进行实现并通过注解进行路由注册,A通过路由对外接口调用B的test方法。
现状:诚然,这种做法没毛病,能够实现功能。但是随着项目模块的增多,function 里面会存在很多的业务模块数据接口。有一种情况:如果存在A,B,C,D,E 5个模块,它们都在function内存放了数据接口,并且5个模块都引用了function模块。那么,当A需要,并且只需要B的数据接口,而不需要C,D,E的接口时,它还是不得不去引用这些用不着的接口。A不需要这些接口,但是,还不得不引用!这显然会不合逻辑。并且这种全部业务数据接口都塞到function模块里面的做法,会导致function出现不必要的臃肿。
理想:每个业务模块的数据接口,只和本模块的业务有关,所以最好是放在本模块之内,但是,如果放在本模块之内,又会导致组件之间不能通信。那么就创建一个专门的Module来存放每个业务模块的接口。想法可行,但是每个业务模块的module数量一下子加倍了,又会造成维护困难的问题。那么有没有方法可以自动生成这些数据接口模块呢?还真有~神奇的gradle编程 >_<
关键词:组件API化技术,使用gradle配置,对module内的特殊后缀文件进行检索,并以当前module为基础,自动生成新的module
实例解析
这个名叫MineOpenServiceApi
的接口,原本是.java
后缀,现在改成.api
import cn.appblog.api.facade.template.IProvider;
/**
* “我的”模块的所有对外公开的数据接口
* 这个文件是api后缀的。如果要编辑接口,在这里改
* 整个项目编译之后,此.api会自动生成一个同名的.java文件。不要去编辑这个.java文件
*/
public interface MineOpenServiceApi extends IProvider {
/**
* 加入这里有一个用户名,需要反馈给外界
*
* @return
*/
public String accountNo();
public void showAccountNo();
}
打开demo的setting.gradle
文件,添加:
include_with_api(':business:fragment_mine')
def include_with_api(String moduleName) {
include(moduleName)
//获得工程根目录
String originDir = project(moduleName).projectDir
//制作的 SDK 工程的目录
String targetDir = "${originDir}_api"
//制作的 SDK 工程的名字
String sdkName = "${project(moduleName).name}_api"
System.out.println("-------------------------------------SDK name:" + sdkName)
//删除掉 SDK 工程目录 除了 iml
FileTree targetFiles = fileTree(targetDir)
targetFiles.exclude "*.iml"
targetFiles.each { File file ->
file.delete()
}
//从待制作SDK工程拷贝目录到 SDK工程 只拷贝目录
copy {
from originDir
into targetDir
//拷贝文件
include '**/*.api'
include '**/AndroidManifest.xml'
include 'api.gradle'
}
//读取实现模块的manifest并将package的值后加 .api 作为API工程的manifest package
FileTree manifests = fileTree(targetDir).include("**/AndroidManifest.xml")
manifests.each {
File file ->
def parser = new XmlParser().parse(file)
def node = parser.attribute('package')
parser.attributes().replace('package', "${node}.api")
new XmlNodePrinter(new PrintWriter(file)).print(parser)
}
//将api.gradle改为build.gradle
File build = new File(targetDir + "/api.gradle")
if (build.exists()) {
build.renameTo(new File(targetDir + "/build.gradle"))
}
// 将.api 文件改为 .java
FileTree files = fileTree(targetDir).include("**/*.api")
files.each {
File file ->
file.renameTo(new File(file.absolutePath.replace(".api", ".java")))
}
//加入 SDK工程
include ":business:" + "$sdkName"
}
这段代码来自一位"真"大神,它的作用是,检索指定模块里面,有没有指定后缀名(.api
)的文件,有的话,找出来,经过一系列处理(注解很详细,应该能看懂),自动生成一个module。生成的module名字比原来的module多一个_api
。表示这个模块,包含原模块的所有对外数据接口。
注意事项
- 数据接口的
.java
后缀需要改成.api
(这个.api
完全和setting.gradle
代码里的.api
对应,可以都换成其他后缀) - 原模块里面,会多出一个
api.gradle
,这个文件的名字也和setting.gradle
里的api.gradle对应,也可以修改 - 这个
api.gradle
并不会在本模块被编译的时候起作用,但是它最终会变成_api
新模块的build.gradle
,并保持完全一样的代码。新的_api
模块只是一个library,所以,要去掉本模块的build.gradle
里面针对isModule
的判定
成果及优势
组件API化的成果:原模块fragment_mine
,自动生成的模块fragment_mine_api
现在不用把所有的数据接口都放到function公共模块内,而只需要在本模块之内将数据接口文件后缀改成.api
,然后在setting.gradle
里面使用自定义的方法进行include。就可以只引用本模块需要的数据接口module,而不需要引用多余的module,而且,防止了function模块的无意义的膨胀。
结语
Demo只是提供一种组件化的全攻略,可能demo的代码并没有十分完善,比如:原ARouter源码内的带参数的跳转,或者startActivityForResult
,由于时间关系我都去除了。一些辅助性的设计思路,也并没有完全遵照ARouter源码。
版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/29/android-componentization-development-custom-routing-framework/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论