前言
手把手讲解系列文章,是我写给各位看官,也是写给我自己的。
文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作5,6年,不能老吸血,也到了回馈开源的时候.
这个系列的文章:
1、用通俗易懂的讲解方式,讲解一门技术的实用价值
2、详细书写源码的追踪,源码截图,绘制类的结构图,尽量详细地解释原理的探索过程
3、提供Github 的 可运行的Demo工程,但是我所提供代码,更多是提供思路,抛砖引玉,请酌情cv
4、集合整理原理探索过程中的一些坑,或者demo的运行过程中的注意事项
5、用gif图,最直观地展示demo运行效果如果觉得细节太细,直接跳过看结论即可。
本人能力有限,如若发现描述不当之处,欢迎留言批评指正。
学到老活到老,路漫漫其修远兮。与众君共勉 !
引子
最近得高人指点,恰巧工作中做过一个移植其他app的某个功能模块的任务,过程简直痛不欲生。
然后思考如何对app的各个功能模块进行灵活拔插配置,最大程度减少移植代码出错的可能性,保证功能模块的完整移植
此次手写架构,解决的问题是:
1、让 App内 各个功能模块能够独立开发单元测试,也可以 所有模块集成打包,统一测试
独立开发
更改gradle.properties的配置,使得每个功能模块都成为application, 可以独立打包成apk,单独运行。单个模块,独立测试。
集成打包
更改gradle.properties的配置,使得原先每个单独模块,都变成library,被 主模块引用,这时候只有主模块能够打包apk,所有功能都集成在这个apk内。
2、实现 功能模块的整体移植,灵活拔插
故事背景
当你们公司有多个安卓开发人员,开发出核心业务相同,但是UI不同,其他业务不同的一系列App时(如果核心业务是X,你们有5个开发人员,做出了A,B,C,D,E 5个app,都包含核心业务X,但是除了X之外,其他的业务模块各不相同)这时候,如果领导要把A里面的一个非核心功能,挪到B里面...
现状
开发B的程序猿可能要骂娘,因为他在从移植A的代码中剥离代码 遇到了很多高耦合,低内聚 的类结构,挪过来之后,牵一发而动全身,动一点小地方,整个代码满江红。
理想
如果这个时候,我们通过代码框架的配置,能够把A里面的一个模块,作为一个module 移植到 工程内部,然后主module 来引用这个module,略微写一些代码来使得这个功能模块在app中生效。那么无论是多少个功能模块,都可以作为整体来 给其他app复用。这样开发人员也不用相互骂娘了,如果挪过来的模块存在bug或者其他问题,也不用甩锅,模块原本是谁开发的,找谁就好了。
3、保证App内 业务模块的相互隔离,但是又不妨碍业务模块之间的数据交互
我们开发app的功能模块,一个业务,可能是通过一个Activity或者 一个Fragment 作为对外的窗口,也可能是。所谓窗口,就是这个业务,相对于其他模块,"有且只有"一个入口,没有任何其他可以触达到这个业务的途径。业务代码之间相互隔离,绝对不可以有相互引用。那么,既然相互不会引用,那A模块一定要用到B模块的数据,怎么办呢?下文提供解决方案。
鸣谢
感谢享学课堂的老师们的公开课 https://ke.qq.com/course/341933
感谢群里兄弟们的旁敲侧击
感谢阿里巴巴大佬们的ARouter开源框架
正文大纲
1、代码结构现状以及理想状态一览
2、功能组件化的实现思路,实现组件移植拔插
3、参考ARouter源码,写出自己的Router框架,统一通过Router来进行模块的切换 以及 组件之间数据的交互
4、使用组件api化,在模块很多的情况下优化公共模块的结构
正文
1、代码结构现状以及理想状态一览
先来看两张图
现状
image.png
代码有模块化的迹象,但是没有对业务模块进行非常明显的模块化(不明白啥意思是吧?不明白就对了,app这个module里面其实还有很多东西没有展示出来,请看下图:试想,把所有的模块集中到一个module的一个包里面,当你要移植某一个功能的时候,想想那酸爽....当然如果你口味别致,那当我没说)
image
理想:
image.png
理想化的话,参照:理想.png; 项目结构层次分明,脉络清晰
按照图中的分层,详细解释一下:
外壳层:app module
内部代码只写 app的骨骼框架,比如说,你的app是这个样子的结构:
典型的app架构.png
下方有N个TAB,通过Fragment来进行切换模块。这种架构肯定不少见。
这个时候,外壳层 app module,就只需要写上 上面这种UI架构的框架代码就行了,至于有多少个模块,需要代码去读取配置进行显示。神马?你问我怎么写这种UI框架?网上一大把,如果实在找不到,来我的 github项目地址
๑乛乛๑
业务层
我们的业务模块,对外接口可能是一个
Activity
(比如说,登录模块,只对外提供一个LoginActivity
,有且仅有这一个窗口)或者 是一个Fragment
,就像上图(典型的app架构.png), 如果app的UI框架是通过切换Fragment
来却换业务模块的话。用business
这个目录,将所有的业务模块包含进去,每个模块又是独立的module
,这样既实现了业务代码隔离,又能一眼看到所有的业务模块,正所谓,一目了然。
功能组件层
每一个业务模块,不可避免的需要用到一些公用工具类,有的是第三方SDK的再次封装,有的是自己的工具类,或者自己写的自定义控件,还有可能是 所有业务模块都需要的 辅助模块,都放在这里。
路由框架层
设计这一层,是想让app内的所有Activity,业务模块Fragment,以及模块之间的数据交互,都由 这一层开放出去的接口来负责
gradle统一配置文件
工程内部的一些全局gradle变量,放在这里,整个工程都有效
module编译设置
setting.gradle 配置要编译的module; 也可以做更复杂的操作,比如,写gradle代码去自动生成一些module,免除人为创建的麻烦.
2. 功能组件化的实现思路,实现组件移植拔插
能够兼顾 每个模块的单独开发,单独测试 和 整体打包,统一测试。 听起来很神奇的样子,但是其实就一个核心:gradle编程。
打开gradle.properties文件:
image
注解应该很清晰了,通过一个全局变量,就可以控制当前是要 模块化单元测试呢?还是要集成打包apk测试。
那么,只写一个isModule就完事了吗?当然不是,还有一堆杂事 需要我们处理,我们要使用这个全局变量。
一堆杂事,分为两类:
image
1- app 外壳层module 的build.gradle(注意:写在dependencies)
if (isModule.toBoolean()) { implementation project(":business:activity_XXX") //...在这里引用更多业务模块}
2- 每个业务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 "XXXX"这一句
defaultConfig { if (!isModule.toBoolean()) { applicationId"study.hank.com.XXXX"* } .... }
第三处:当业务模块module作为library的时候,不可以在 AndroidManifest.xml中写 Launcher Activity,否则,你打包app module的时候,安装完毕,手机桌面上将会出现不止一个icon。而,当业务模块module 作为application单独运行的时候,必须有一个Launcher Activity ,不然...launcher都没有,你测个球 ``` 所以这里针对manifest文件进行区分对待。
sourceSets { main { if (isModule.toBoolean()) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } }
由于要区分对待,我们就需要另外创建一个manifest文件,移除launcher配置即可。参考下图:
image
image
这就是业务模块组件化的秘密了。
什么,你问我怎么 进行功能拔插?
当你不需要某一个模块的时候,
1)在app的build.gradle里面 把 引用该模块的配置去掉;
image.png
2)setting.gradle 的include 去掉它
image.png
3)app module 里面,改动代码,不再使用这个模块。(这个我就不截图了,因为app module的UI框架代码不是一句话说得清的。请运行我的demo源码自己看吧)
功能的插入,同理,上面的过程倒过来走一遍,就不浪费篇幅了。
3. 参考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();
作者:波澜步惊
链接:https://www.jianshu.com/p/e73bc515ab53