Android移动应用ALL IN ONE架构衍变

Android移动开发 专栏收录该内容
19 篇文章 0 订阅

这篇文章更多表达的是一种对架构的思考:我们是否保持一颗开放、积极的心态去拥抱变化。我们不可能在市面上找到适合我们的架构,脱离业务谈架构是没有任何意义的,适合业务的才是好架构。真正好的架构源于不停地衍变,而非设计

架构设计中的思考

世间唯一不变的是变化本身,因此架构进化以适应不断发生的变化是必然的选择。没有任何事物是生而完美的,随着时间推移,现实约束会发生变化,技术会不断发展,资源条件也会出现转机,因此,对架构提出新的需求,进化时机也就随之而来。就像淘宝之初,只是简单的买家网站,经过十年的不断演进,才有今天如此复杂庞大的系统,轻松应对双十一。

为什么架构需要不停地衍变

在一个长期没有改进与变化的框架下,开发者的习惯可能会逐步变成跟随式、保守式的开发。这大概可以被描述成“只要别人这样做,我也这样做,哪怕这么样的设计不好,但也不会错”。随着心态逐渐普遍,另一种情形出现:经常能听到有同学吐槽一些代码,却更少看到代码在被改进。这说明一些沉积的问题不是没有被大家发现,只是没有人愿意去修改。这种情况下代码和框架会随着时间变得越来越差,有些问题逐渐变成“陈年旧病”。 面对这个问题首先要说,这不是开发者合格与否的问题,实际上有想法的开发人员有很多,但想将每个想法转换成代码并让大家接受,并不是一件很容易的事。尤其在一个大框架下,尝试改变的代价很大。如果他的主要任务不在改进某些模块上,那么很多想法最后都无法变成现实。这也是为什么保守和跟随的习惯会逐渐变的普遍

保守和跟随的气氛需要被打破。当开启一次重构之后,你会发现团队中会有很多积极的声音响应,他们会把积压的想法和意见抛出来。一次问题的解决,可能会为一堆问题的解决带来机会,也会让一些有想法的开发同学得到的表达机会,这个团队才是有活力的。只有充满活力的团队才能创造出充满活力的代码

对自己的模块负责

有这样一句话:“不被监管的权利一定会发生腐败” 。如果放到软件开发的行当来说,就是“不被监管的代码也一定会发生劣化”。所以代码应该要接受“监管”,代码审查的好处毋庸置疑。正常下代码审查是由leader进行review,这容易导致效率较低且容易遗漏问题,特别是一些基础的支撑工程,被无数人维护过,导致了“无主代码”特别多。大家缺少对代码的“归属感”,也降低了改进优化模块的欲望。合理的代码审查更应该是全员性质的。

我们在架构设计中采用模块化的思路,APP各个业务组件是由各个可管理的子模块构成的,每个子模块之前相互独立,同级之间没有依赖。比如扫码模块,分享模块、运动模块,这些模块以“aar”的方被各个业务组件依赖,他们之间没有关联。通过大家认领模块,对模块的代码和设计负责,对模块对外提供的接口服务负责,对其他人修改自己模块的行为进行监督。这些情况明显提高开发同学的代码所有感,改变大家修改优化和修改代码的动机。

不要相信约定或准则

代码的边界就像一堵墙,架构的劣化都是从这堵墙的瓦解开始的。从我们以往的经验来看,单纯的约定或准则并不能永远的保持下去,编译上的隔离是最好的约束手段,所以在任何情况下都尽可能不要放开编译上的约束

我们在设计架构的时候就约定:不同业务模块之间不能有依赖。这个约定没问题,大家都执行,不过有一天,可能是不小心业务A依赖了业务B,也没人知道。慢慢的,这个依赖越来越多,最终导致架构的劣化。

因为嫌弃麻烦放弃编译的约束,单纯靠约定,久而久之必然会导致代码的腐败。我们可以尝试用Groovy插件管理与限制开发同学的行为,直接在编译阶段将腐败扼杀在摇篮之中。比如我们可以通过Gradle插件统一compileSdkVersionminSdkVersiontargetSdkVersion,统一 support 等常用第三方库的版本、检查 git commit 的信息是否规范等。可参考通过Gradle插件统一规范

面向过程还是面向对象

佛理之中有句名言:“心中有佛,看人即佛”。心中有佛,眼里万物皆为佛,而心里不静,则眼中不净,相由心生。这句话用在程序编程当中也是一样:心中只有过程,满眼都是过程

当我们习惯了面向过程编程时,发现在编程过程中到处找不到需要面向对象的地方,最主要的原因,是思维没有转变。程序员通常在拿到一个需求的时候,第一个反应就是如何实现这个需求,这是典型的面向过程的思维过程,而且很快可能就实现了它。而面向对象,面对的却是客体,第一步不是考虑如何实现需求,而是进行需求分析,就是根据需求找到其中的客体,再找到这些客体之间的联系。因此面向过程和面向对象的思维转变的关键点,就是在第一步设计,拿到需求后,一定先不要考虑如何实现它,而是通过UML建模,然后按照UML模型去实现它。这种思路的转变,可能需要个过程。

举个需求例子:有两个页面都需要用到地图,而这两个地图页面稍微有一些差别,需要你实现这个逻辑。你首先想到的是我先选个第三方的地图,比如谷歌地图,然后你用谷歌实现其中的地图页面,最后再复制一份代码修改实现另外一个地图页面。当有一天增加了第三个地图页面,你又要复制代码,更夸张的是产品说我们不能用谷歌地图,改为百度地图,这时候你慌了:完了呀。我们用面向对象的逻辑思考:我们定义一个通用的地图接口,我们可以扩展出谷歌地图的实现类以及百度地图的实现类,最后直接用地图接口协议去实现地图页面而不用考虑采用什么地图,面对各种需求都能轻松应对。

面向对象能实现人们追求的系统可维护性,可扩展性,可重用性。这个思想在我们架构设计当中是重中之重。面向对象的内功是可以修炼的,多看多想多用,心中有对象,万物皆对象。可参考Android常用面向对象设计模式

架构设计中的概念

对程序进行架构设计的原因,归根到底是为了提高生产力。通过设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合。架构设计发展至今,业界各种Android客户端架构设计五花八门,我们会经常听到各种技术专业名词:模块化、组件化、插件化,热修复、热更新、热加载等等,有没有一头雾水?下面我们就来聊聊这些个技术~

模块化

模块的英文是module,源于Android studio提出的模块化概念。Android Studio工程支持多个module同时开发,module分为application module(应用模块)和library module(库模块):

  • application module:可以直接运行在Android设备上,最终生成apk执行文件。
  • library module:不能运行,以library库的方式被application module所依赖,最终生成aar库文件。

什么是模块化?

我们在架构设计中把常用的功能、控件、基础类、第三方库、权限等公共部分抽离封装成独立的模块,以及把单个业务拆分成多个模块进行独立管理,同级模块之间不能有依赖。这就是模块化在架构设计中的实践。广义上说:将一个复杂业务实现,根据功能、页面或者其他进行不同粒度的划分程不同的模块,模块之间解耦,分别进行实现,也就是编程的模块化思想。

下图工程Structure中分贝有四个组件,app是应用组件,可以运行在设备之中,moduleAmoduleBmoduleC是库组件,可以被app所依赖,但是他们之间不能有依赖关系。

在这里插入图片描述
如果我们的架构设计仅仅按照模块化的思想,假如工程越来越大,我们的模块越来越多,最后会导致以下问题:

  • 项目可维护性下降:随着项目的增加,即使有做分包目录,但是项目会逐渐失去层次感,可读性、可维护性下降,多人联合开发时,在版本管理中容易出现冲突和代码覆盖问题。
  • 开发和调试效率低:开发和调试时,修改了一个小功能,但是需要重新build整个项目才能看到结果,编译时间长。
  • 易阻断不同业务模块的并行开发:一个业务模块的小bug,可能阻断其他业务模块的开发和调试,不同业务模块的并发开发会被阻断。

组件化

将一个app按照业务(功能)划分为多个模块,每个模块都是一个组件(Module),组件之间是不能直接耦合的,可以独立编译打包apk,也可以将各个组件组合打包到一个apk中。这就是组件化思想,组件数 <= 模块数,实际上可看做是个大模块,组件实际上是包含若干模块的组合。

组件化后的优点:

  • 项目可维护性高:开发只负责自己的模块,还可以再做的隔离一些,每个业务线只可见自己业务模块的代码,避免了误修改和版本管理问题。
  • 开发和调试提高:每个模块可独立编译,也可以作为库被主工程依赖,提高了编译速度,加快开发效率;
  • 加快并行开发效率:不同业务之间并行开发,不受影响。

与模块化的区别:组件可以进行角色的转换,一个组件可以独立编译打包,也可以作为lib集成到整个apk中。

插件化

插件化是将一个apk根据业务功能拆分成不同的子apk(也就是不同的插件),每个子apk可以独立编译打包,最终发布上线的是集成后的apk。

在这里插入图片描述
原理
当我们点击相应的模块时,就会从服务器下载相应的插件APK或者Library,存放到本地指定目录下,下载完之后我们就会用DexClassLoader去动态加载这个apk里面的资源对象(我们需要在apk里面用到这些资源) 以及类加载器(第三方插件apk是不具备生命周期以及没有上下文),通过插件apk的类加载器获取到插件化apk中的Activity的类对象,然后进行跳转。

组件化与插件化详细对比

技术单位实现内容灵活性特性静动态
组件化module是解耦与加快编译,隔离不需要关注的部分按加载时机切换,是作为lib,还是apk组:组本来就是一个系统,每个组件不是真正意义上的独立模块静态加载
插件化apk是解耦与加快编译,同时实现按需加载的热插拔加载的是apk,可以动态下载,动态更新,比组件化更灵活插:是独立的apk,每个插件可以作为一个完全独立的apk运行,也可以和其他插件集成为大apk动态加载,只用真正使用某个插件时,才加载该插件

随着web前端技术的发展,插件化技术已逐渐被抛弃。虽然技术上是没问题的,而且大公司的产品也有不少已经在用,但是要实践会有很多的限制和坑,需要耗费大量的人力去维护,我们需要慎重考虑到投入产出。目前Apple StoreGoogle Play已经禁止,只能在国内的应用市场使用。类似微信这么庞大的应用也放弃插件化架构,而采用组件化+tinker热修复的方式。

常用的插件化框架

  • RePlugin
    RePlugin是360出品的,历经三年多考验,数亿设备使用的,稳定占坑类插件化方案。
  • Shadow
    Shadow是一个腾讯自主研发的Android插件框架,经过线上亿级用户量检验。

当然还有:SmallDroidPluginAtlas等方案。

Google推出自身的插件话方案

市面上的插件化方案,Google官方是明面禁止的。但是,面对越来越强烈针对减少安装包体积和按需加载的插件化需求,官方不得不自己开发出一套插件化方案:Android App Bundles。目前是官方允许的,但是使用上会存在诸多限制。

Android App Bundles

aab文件是Android App Bundle编译后生成的文件格式。不同于以往的apk文件,aab文件并不能直接在Android设备上安装和运行,aab格式的文件是专门用来提交到Google Play使用的。Google Play使用后文所说的split APKs机制将一个aab文件转换为若干个apk文件,用户在下载app的时候,再通过Dynamic Delivery将用户需要的apk文件提供给用户。AAB可以理解为一款全新的动态化框架,强调的是减少app包体积同时提供一样的用户功能体验,提供按需下载安装模式。

局限性

  • 需要Google Play Service支持,国内环境无法使用。
  • 需要上传aab文件,经过Google Play审核通过才能用。
  • 不支持动态更新四大组件,比如Activity。只能更新资源文件以及非四大组件代码。
  • 最低支持版本Android 5.0 (API level 21)。
  • 需要升级到Android Studio 3.2修改工程以便支持App Bundle格式。
  • 需要集成Play Core Library

最终用不用,还需全面考量,如果APP应用大小能接受,而且接入会带来风险,可以暂缓App Bundles计划。

热修复

首先需要明确的一点,插件化和热修复不是同一个概念,虽然站在技术实现的角度来说,他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;但是二者的出发点是不同的。插件化顾名思义,更多是想把需要实现的模块或功能当做一个独立的提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。热修复则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。

原理
说起热修复就不得不提类的加载机制,和常规的JVM类似,在Android中类的加载也是通过ClassLoader来完成,具体来说就是PathClassLoaderDexClassLoader 这两个Android专用的类加载器,区别如下:

  • PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,也就是我们一开始提到的补丁。

热修复的原理是将修复代码打包成补丁文件,然后通过这个补丁文件封装出一个Element对象,并将这个Element对象插到原有的DexElements数组的最前端,当DexClassLoader去加载类时,优先会从我们插入的这个Element中找到相应的类,虽然那个有bug的类还存在于数组中后面的Element中,但由于双亲加载机制的特点,这个有bug的类已经没有机会被加载了,这样一个bug就在没有重新安装应用的情况下修复了。

双亲加载机制:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的BootStrap ClassLoader(启动类加载器)进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

常用的热修复框架

  • tinker
    腾讯出品,Tinker是一个针对Android的热修复解决方案库,它支持dex、库和资源更新,而无需重新安装apk。
  • Andfix
    AndFix是一个在线修复漏洞的解决方案,而不是重新发布Android应用程序。它作为Android库分发。

当然还有其他的方案:HotfixAmigoRobust

热更新

指的就是热修复,属于同个概念不同的表述而已。

热加载

热加载的实现原理主要依赖java的类加载机制,在实现方式可以概括为在容器启动的时候起一条后台线程,定时的检测类文件的时间戳变化,如果类的时间戳变掉了,则将类重新载入。对比反射机制,反射是在运行时获取类信息,通过动态的调用来改变程序行为;热加载则是在运行时通过重新加载改变类信息,直接改变程序行为。热加载可以极大地提高开发效率。在应用运行时升级软件,无需重新启动的方式就叫热加载。

从概念上看,插件化、热修复、热更新等机制都可以叫热加载,都可以在不需要重启应用的情况下实现动态加载。

谈谈我们1.0版本的APP架构

我们的应用形态属于"ALL IN ONE"硬件设备辅助类型的APP,类似小米家族的“米家”APP,可以集成各类的设备管理功能。有一个特点是:动态性,也就是随着时间的推移,支持的设备数量越来越多,APP的应用越来越大。这样形态的应用架构要怎么设计呢?

架构选型

由于我们的应用主要销往国外,必然要上架Google Play以及Apple Store插件化以及热修复的技术只能放弃(人家不允许你用),只剩下组件化以及模块化两个选项。针对这两个选项,也没什么好纠结的,模块化是不可能的,模块化的弊端前面已经分析过,所以就只有一个选择了:组件化。同时,我们团队放弃了Acticvity作为页面单元,全部采用Fragment作为页面路由单元,所以最后的架构叫:基于单Activity多Fragment框架的组件化架构

在这里插入图片描述
架构特点:

  • 模块层以单个aar压缩包为基本单元,相互隔离,采用模块负责人制度
  • 组件层按需选择依赖模块单元。
  • 组件层所有的组件都要依赖common组件
  • APP组件是所有组件的公共部分,也就是首页,其他组件属于设备详情页。
  • 运行调试的应用组件只有APP组件,其他组件都是以library库的方式存在。
  • 可通过currentModule配置哪些O Library库加入编译调试,按需编译按需调试。

存在的现状:

  • 业务组件不可以单独调试或者打成APK,必须要集成到APP组件才能运行调试。
    解决方案:组件化的一个基本准则就是单个业务单独调试能力,从这个角度来讲,我们的架构并不算是组件化架构,而是动态配置的模块化架构。最终我们应该让业务组件具备单独调试的能力。
  • 部分aar没有版本的区分,导致无法自动更新代码提示。
    解决方案:aar增加版本号。
  • 模块的封装基本都是基于面向过程封装的。
    解决方案:培养我们的面向对象编程能力,多看设计模式,转变思维方式。

common组件分析

由架构图分析可知,全部的组件都会依赖common组件。作为一个公共的组件,它目前的结构组成是怎么样的呢?
在这里插入图片描述
common组件特点:

  • 定义了一些与业务强绑定的公共实体,变量以及全部页面路由路径,如设备PID的全部定义列表等。
  • 封装了一些与业务无关的工具类,比如时间转换,APP状态管理、异步处理等工具。
  • 封装了一些与业务强绑定的工具类,比如数据库缓存、FireBase统计、消息推送等工具。
  • 集合了所有组件共有的国际化字符串翻译。

存在的现状:

  • 存在不应该放在这个组件的类。比如自定义控件自动缩放Textview
    解决方案:删除自定义控件自动缩放Textview
  • 组件代码边界不可控,缺乏约束,将来越来越多的无人维护的代码将会放这里,可能导致野蛮生长。
    解决方案:增加代码边界约束,简化组件职责。
  • 存放过多业务强绑定的逻辑代码,比如FireBaseNotification部分,导致后期维护困难。
    解决方案FireBaseNotification独立封装成,下沉到业务模块,单独维护。
  • 没有存放公共主题的图片,导致每个业务组件都要复制一份图片资源,加大安装包体积。
    解决方案:公共主题图片统一放在common组件,后期common组件只作为资源组件维护即可,尽可能不放业务逻辑。
  • common组件如何拆装问题
    解决方案:1、抽离出部分业务组件,同时拆分为resource资源组件和common组件,可能增加编译时间;2、抽离出部分业务组件,资源和一些共享代码同时放这里。
  • utils工具类存在重复,相互引用修改等现象
    解决方案:制定utils工具类良好的管理策略。

单Activity多Fragment+MVP框架

Activity是一个非常重量级的设计!Activity的创建并不能由开发者自己控制,它是通过多进程远程调用并最终通过放射的方式创建的。在此期间,AMS需要做大量的工作,以至于Activity的启动过程极其缓慢。同时,Activity切换的开销也非常重量级,很容易造成卡顿,用户体验不好。另外,在宽屏设备上,如果需要多屏互动时,Activity的局限性也就表现了出来。为此Android团队在Android 3.0的时候引入了FragmentFragment的出现解决了上面两个问题。根据词海的翻译可以译为:碎片、片段,可以很好解决Activity间的切换不流畅。因为是轻量切换,性能更好,更加灵活。

我们重新定义了Activity以及Fragment的生命周期,高度封装了一套单Activity多Fragment框架,扩展了页面跳转逻辑、权限管理,定时器管理,百分比布局、EventBus总线数据传输等功能。最后形成的页面单元结构如下:

public class Frag_xxx extends RootFrag {

    @Override
    public int onInflateLayout() {
        return R.layout.xxx;//传入layout页面布局文件
    }

    @Override
    public void initViewFinish(View view) {
        //layout页面处理完成,可以做页面初始化的工作,如设置页面默认值等
    }

    @Override
    public void onNexts(Object o, View view, String s) {
		//所有页面处理逻辑完成,携带页面跳转的数据单元,可以做业务逻辑,比如网络请求,数据库加载等操作
    }

    @Override
    public boolean onBackPresss() {
        return false;//单个页面决定要不要自行处理回退事件
    }
}

框架优势:

  • 页面结构简单,功能完善,上手速度快。
  • 内部高度封装页面跳转、权限管理、定时器管理等功能,减少开发周期,减少bug率和崩溃率。

存在的现状:

  • 框架内部强依赖butterknifeeventbusfastjson等第三方框架,加大后期升级维护的难度。
    解决方案:框架内部减少第三方依赖,让框架更轻量,可采用其他替代方案。
  • Fragment页面消失之后需要手动清理内存,无法做到自动的内存管理。
    解决方案:自动管理Fragment内存,页面退出可自动清楚内存。
  • 页面跳转以及页面回退操作需要手动指定页面,无路由栈管理。
    解决方案:增加路由栈管理,自动管理页面回退。

Android中的三种设计模式

我们的1.0版本架构中页面约定逻辑采用的是“单Activity多Fragment+MVP框架”。

MVC

MVCModel View Controller的缩写,是一种典型的设计开发模式。其中Model为模型,View为视图,Controller为控制器。它的模式设计图如下所示:
在这里插入图片描述
从上面的工作流程可以得知,Controller持有了ViewModel对象,Model持有View对象。在Android中,View对应着xml布局文件,Model对应着实体模型(网络、数据库、IO等),Controller对应着Activity/Fragment的业务处理,数据处理,UI展示等逻辑。关系如下:

M:网络、数据库、IO
Vxml布局文件
PActivity/Fragment

为什么不能用MVC模式:

  • 在Android实际开发中,MVC开发模式中的Activity/Fragment既要负责View层的更新,同时也负责Controller层对逻辑的控制,如果一个功能比较复杂,那么Activity/Fragment要承担的任务就非常繁重,导致代码量非常庞大,难以维护。
  • Mode层和View层之间存在着耦合关系,给后期的维护带来巨大的工作量。
MVP

MVPModel View Presenter的缩写。其中Model为模型,View为视图,Presenter为桥接器。针对MVC设计模式中Activity/Fragment的工作量巨大,代码逻辑比较复杂等缺点进行了一定的改进,将代码控制逻辑单独拆分开,放在Presenter模块。它的模式设计图如下所示:
在这里插入图片描述
在Android中,View对应着xml布局文件和Activity/Fragment,Model对应着实体模型(网络、数据库、IO等),Presenter作为ViewModel两者之间的桥梁,对业务逻辑进行操控,这样就将ViewModel进行了解耦,它们两者都由Presenter进行逻辑之间的操控。关系如下:

M:网络、数据库、IO
Vxml布局文件和Activity/Fragment
PPresenter桥接器

特别注意

  • MVCView层指的是XML布局文件或者是用Java自定义的ViewMVPView层是xml布局文件和Activity/Fragment
  • 默认一个View对应着一个Presenter,但是一个View比较复杂可以一个View对应多个Presenter,或者View复用性比较强,也可以多个View对应一个Presenter,具体要看业务,灵活使用。

为什么用MVP模式:

  • MVP模式下ModelView完全分离开,两者之间没有任何耦合,维护简单。
  • 减少了Activity/Fragment的职责,将复杂的逻辑代码提取到了Presenter中进行处理,模块职责划分明显,层次清晰。
  • 耦合度更低,更方便的进行测试。

MVP模式同时因为Presenter持有了ModelView的引用,其中包含了大量的控制逻辑,可能会使得Presenter变得复杂而庞大,后期维护会比较困难。对于这点,有一个方法是在UI层和Presenter之间设置中介者Mediator,将例如数据校验、组装在内的轻量级逻辑操作放在Mediator中;在PresenterModel之间使用代理Proxy;通过上述两者分担一部分Presenter的逻辑操作,但整体框架的控制权还是在Presenter手中。MediatorProxy不是必须的,只在Presenter负担过大时才建议使用。

在这里插入图片描述

MVVM

MVVMModel View ViewModel的缩写。其中Model为模型,View为视图,ViewModelView的数据模型和Presenter的合体,其实就是将MVP中的Presenter替换成了ViewModel,并通过双向数据绑定来实现ViewViewModel的交互。而在Android中一般使用Data Binding来实现双向的数据绑定。它的模式设计图如下所示:
在这里插入图片描述
为什么不用MVVM模式:

  • 面异常可能是View的问题,也可能是Model的问题,数据绑定使得bug很难被发现。
  • 数据绑定让一个View和一个Model绑定起来,不同模块的Model基本上是不同的,数据双向绑定不利于代码重用。
  • MVVM比较适合页面变化比较频繁的场景,Android采用这个模式相对比较少。

架构中MVP模式的使用情况

在我们MVP设计模式中,有一些最基本原则需要遵守:
1、View对应的是Activity/Fragment(在我们的框架中对应Fragment)。
2、大部分情况下一个View对应一个Presenter(在我们的框架中叫Helper)。
3、View不能跟Model有耦合(不能直接调用数据库、IO、网络请求等操作)。
4、View只能负责页面显示相关,数据校验、组装等都应该放在Presenter中。
5、Presenter较大的情况下可以采用MediatorProxy减轻负担。

我们照着上面五个MVP的基本原则看我们Fragment中的代码,发现以下问题:

  • 大部分Fragment没有对应的基本Presenter
    解决方案:具有用户交互的View创建对应的Presenter中间介。
  • 部分Fragment中存在直接的数据库操作。
    解决方案FragmentModel不能有耦合,将Model的操作移到Presenter
  • 部分Fragment中存在数据校验等相关逻辑。
    解决方案Fragment只对UI显示负责,其他逻辑移到Presenter中。
  • 统一采用Helper结尾命名,无法区分哪些是业务的Presenter,哪些是工具类的Presenter
    解决方案:1、工程目录增加一级presenter结构,用来存放Fragment对应的Presenterhelper目录用来存放与Fragment无关的工具类Presenter;2、Helper只用于业务相关定义,业务无关的移到Utils目录,并以Util结尾命名。
  • 不同Fragment中风格不一,部分存在臃肿现象,后期不好维护。
    解决方案:这个现象是因为没有循序MVP模式造成的,其实框架中的生命周期定义的颗粒度基本满足,只要我们抽离出PresenterFragment中的代码会很干净,有序,可以达到看起来是同个人写的效果,方便后期维护。
  • Helper内部代码结构没有章法,风格不一。
    解决方案:指定统一的Helper编码规则,让代码看起来像一个人写的一样。

谈谈我们APP架构2.0演变思路

在前面我们总结了当前框架中存在的一些现状,下面我们重点就这些个解决方案给出用例。

组件化方案

组件化后的每一个业务的module都可以是一个单独的APP,也可以是一个library,需要我们做模式切换策略。下面的策略是通过 gradle 脚本来实现的。

1、在工程根目录的 gradle.properties 中定义一个全局变量(也可以放在其他地方)

isDebug = false

2、在App组件(负责打包的ALL IN ONE 组件或者壳组件)的 build.gradle 动态依赖

dependencies {
    if(!isDebug.toBoolean()){
        api project(':mtxx')
        api project(':mkxx')
        api project(':roxx')
    }
}

3、在所有需要模式切换的业务组件 build.gradle 中增加模式切换代码

if (isDebug.toBoolean()) {//切换为调试模式
    apply plugin: 'com.android.application'
} else {//切换为library模式
    apply plugin: 'com.android.library'
}

android {
	defaultConfig {
		// 作为library时不能有applicationId,只有作为一个独立应用时才能够如下设置
		if (isDebug.toBoolean()){
			applicationId "com.yhd.xxx"
		}
		...
	}
	//因为application和library的AndroidManifest不同,在调试下是存在自定义的 Application等,需要做区分
	//新建debug目录,创建该目录下的AndroidManifest.xm
	sourceSets {
        main {
            if (isDebug.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java { //release 时src/main/java/debug目录下文件不需要合并到主工程
                    exclude 'debug/**'
                }
            }
        }
    }
}

4、创建src/main/java/debug目录,放调试相关的Java类如Application

在这里插入图片描述
到此,组件化配置就结束了,我们做单个组件调试的时候只要将 isDebug 改为 true 即可,非常方便,大大多减了调试时间。当然了,想完全适配组件化还是需要做一些适配工作的,比如组件的Application的数据初始化组件怎么获取登陆的token组件怎么模拟假数据等问题。这些都不难,工作量也不大,一步一步实现即可。

组件通信方案

前面提到,业务组件之间不能有依赖,不过,虽然不能有依赖,组件之间的通信却是时时刻刻存在的。这里的通信包括三个方面:
1、组件之间互相页面跳转
2、组件之间互相监听状态、回调
3、组件之间互相调用类、接口、方法

1、组件之间页面跳转

  • 隐式跳转
    这是我们目前ALL IN ONE架构采用的方式:首先在common组件中定义好全部隐式跳转所需的action标记,同时需要目标跨组件Activity支持我们的协议(manifest中的Activity增加action标记)方可使用这个方案。

    Intent intent = new Intent("this is an action");
    startActivity(intent);
    

    这种方案会有一些限制:要求双方都要按照约定添加action,只要任何一方不小心修改了或者误删以及 漏加约定,运行的时候对方是无感知,出现问题不能在编译阶段发现。

  • 反射
    我们可以通过 Class.forName("xxx"),通过反射的方式拿到这个Class,然后再进行跳转。

    Class taretClass = Class.forName("com.yhd.xxxClasss");
    Intent intent = new Intent(this,class);
    startActivity(intent);
    

    但是这是最 low 方式了,当你这样用的时候,以后包名类名都不敢换了。就好像在黑夹子里面摸东西一样,摸得到就用,摸不到就出bug。

  • URL Scheme
    scheme是一种页面内跳转协议,可以跨应用跨组件跳转页面,需要在Mainefest中配置需要用scheme协议跳转的Activity

    <activity android:name=".TestActivity">
        <intent-filter>
        	<!--配置scheme-->
            <data android:scheme="myscheme" />
        	<!--下面这几行也必须得设置-->
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
        </intent-filter>
    </activity>
    

    跳转调用也简单,参数可以使用类似 url param 的形式:

    String url = "myscheme://scheme-detail/?id=1234&guest=true";
    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url ));
    startActivity(intent);
    

    这种用法更像是HTTP域名以及接口访问,更容易被接受,使用也更为广泛一些。不过要双方约定好协议,也存在协议更换对方无法快速感知以及相应的问题。

  • 跨组件调用内部类实现页面跳转
    我们在下面第三点讲的组件之间互相调用类、接口、方法可以包含页面跳转功能,详细下面讲。

2、组件之间互相监听状态、回调

想要实现跨越组件进行状态监听以及响应,无非是BroadcastContentProviderEventbus等。

  • Broadcast
    Android四大组件之一,没有可视化界面,用于不同组件和多线程之间的通信。可以做到不受其他组件生命周期影响,即使应用程序被关闭,也能接收广播。由于其耗电,占内存的特点,我们还需慎用。

    同时,如果组件之间通信大量使用广播的话,需要双方约定好Key以及Value协议,存在协议更换对方无法快速感知以及相应的问题。如果把这些keyValue数据结构下沉到公共组件,可能造成公共组件不断膨胀,难以维护。

  • ContentProvider
    ContentProviderAndroid组件之一,可以提供数据的跨应用程序访问,提供数据的跨进程无缝隙访问。但是,他偏向的树数据存储共享,我们实际场景用的不是非常多,一般的组件通信不涉及到数据共享。

  • EventBus
    在1.0的架构中,我们大量适用EventBus事件总线作为模块间通信的方式,也基本认为是是除了广播以外是唯一的方式。使用EventBus作为通信的媒介,它传输的是数据结构,自然要有定义它的地方,好让模块之间都能知道EventBus结构是怎样的。这时候公共组件好像就成了存放EventBus的唯一选择:EventBus数据结构定义被放在公共组件中;接着,遇到某个模块A想使用模块B的数据结构类,怎么办?把类下沉到公共组件;遇到模块A想用模块B的某个接口返回个数据,EventBus好像不太适合?那就把代码下沉到基础工程吧!就这样越来越多的代码很“自然的”被下沉到基础工程中。

    EventBus并非所有通信需要的最佳形式。它的特点适合一对多的广播场景,依赖关系弱。一旦遇到需要一组业务接口时,用EventBus写起来那是十分痛苦的。也正因如此,这种情况下大家都跳过了EventBus的使用,直接将代码下沉到了基础工程,共享代码,进而导致公共组件的不断膨胀。

    并不是说EventBus不能使用,要特别注意共享数据结构的位置安放,如果说其他方案带来的成本比EventBus高,可以采用折中方案:将这些数据结构从公共组件抽离,单独维护,防止公共组件臃肿,不利于后期维护

3、组件之间互相调用类、接口、方法

目前市面上的应用大量采用Router路由方案做跨组件API访问以及页面跳转,当然,也有一些APP比如微信、美团等直接抛弃了这种做法,选择自认为更优方案:ServiceLoader(暴露组件SDK方式),下面我们详细分析。

  • Router路由
    Router路由提供的具体功能包括Native页面跳转,URL页面跳转,获取Fragment,提供跨组件接口调用以及拦截器等。用的比较多的是ARouter

    原理:不同组件之间统一通过Key-Value的方式,在编译期间向中央Router路由器注册自己new出来的类实例,在运行期间,通过Key找到类,然后执行相关的API
    在这里插入图片描述
    Router路由器非常强大,有点也非常多,所以使用广泛。但是同时也会有它的局限性:

    • 因为需要提前注册,所以需要有全局Router初始化的操作。
    • 需要每个组件都要依赖Router,同时向它注册自己的实例。
    • 需要维护约定的路由路径以及跳转协议,协议更换对方无法快速感知以及相应的问题。
    • 跨组件访问API,会产生大量数据结构。这些交互的接口需要与Router绑定注册,不够灵活。
  • ServiceLoader
    微信架构中,模块通信采用模块提供SDK的方式作为它与其他模块进行通信的手段。通常SDK提供的是什么,是接口 + 数据结构。这种方式好处明显:实现简单也能解决问题,IDE容易补全、调用接口方便,不用配合工具,协议变化直接反映在编译上,维护接口也简单了。

    美团外卖架构中同样是采用这个方式,进一步明确指出采用ServiceLoader的方案,不过需要在官方的ServiceLoader再做一层封装。
    在这里插入图片描述
    模块暴露SDK的方式无非就是新建个SDK工程,剥离接口和数据结构到该工程里面,打出jar包,然后让其他模块引用编译。其他模块就是根据jar包以及配置文件,找这个接口对应的实现类全路径名,然后使用Class.forName("xxx")的反射机制完成类的加载。

    说到反射,可能有些人会担心官方会禁止ServiceLoader,因为他用了反射。我们去看看官方针对非 SDK 接口的限制规则。从 Android 9(API 级别 28)开始,此平台对应用能使用的非 SDK 接口实施了限制。这里的非 SDK 接口指的就是官方SDK中的私用方法,如private方法,因为这些接口可能会在不另行通知的情况下随时发生更改,可能导致你应用不稳定,所以不给你反射来用。我们自己创建的接口并不属于此列,反射不受限制。

    下面简要说下步骤:

    一、创建一个接口文件和他的实现类文件

    package com.service;
    public interface ICallback {//主要做模块通信回调的参数,演示复杂数据结构
        void onSuccess(String name);
    }
    
    package com.service;
    public interface IMyServiceLoader {//定义模块通信的接口
        void getName(ICallback iCallback);
    }
    
    package com.service;
    public class MyServiceLoaderImpl1 implements IMyServiceLoader {//接口实现类1
        @Override
        public void getName(ICallback iCallback) {
            iCallback.onSuccess("name1");
        }
    }
    
    package com.service;
    public class MyServiceLoaderImpl2 implements IMyServiceLoader {//接口实现类2
        @Override
        public void getName(ICallback iCallback) {
            iCallback.onSuccess("name2");
        }
    }
    

    二、在resources资源目录下创建META-INF/services文件夹以及文件,以接口全名命名,内容为所有接口实现类的包名:
    在这里插入图片描述
    三、将IMyServiceLoader.java打出jar给其他模块调用

    ServiceLoader<IMyServiceLoader> serviceLoader = ServiceLoader.load(IMyServiceLoader.class);
    for(IMyServiceLoader service : serviceLoader) {
        service.getName(new ICallback() {
            @Override
            public void onSuccess(String name) {
                Log.v("TAG",name);
            }
        });
    }
    

    这种方式好处很多,最主要的是隔离思想:模块方提供SDK,调用方傻瓜式调用即可,对方有修改直接反应在编译上,不会有任何类需要重新定义或者下沉到公共组件,非常适合模块隔离以及模块通信。具体原理可参考Android模块开发之SPI。当然了,有人会说还要输出jar以及配置文件,太麻烦了,针对这个问题:

    1.Google推出AutoService,自动生成META-INF/services下的配置文件
    2.微信团队将需要输出的jar类文件添加.api后缀,通过gradle脚本自动创建新的工程生成jar

    实际使用中,我们不会直接使用原生的ServiceLoader,需要二次封装,因为他有以下两点缺陷:

    • 反射得到的类是一个新的实例对象,针对单例的场景,需要我们做实例单例化的缓存处理。
    • 因为是反射,所以不支持传递构造方法,如果有传递Context对象的需求需要特殊处理。

MVP中的Presenter约定

关于AndroidMVP模式其实一直没有一个统一的实现方式,不同的人由于个人理解的不同,进而产生了很多不同的实现方式,其实很难去说哪一种更好,哪一种不好,针对不同的场合,不同的实现方式都有各自的优缺点。我们知道Presenter有一个缺陷:业务复杂容易导致臃肿,所以怎么拆分Presenter成为MVP的架构之争。

Presenter = Presenter+Proxy代理

AndroidMVP这个有着5.9k个的star的开原架构提供了一个MVP实现方式,按照这个实现方式,用登录页面来举例子,需要创建四个文件:LoginView页面,ILoginView接口,LoginPresenter中间接以及LoginInterator交互器。他们的作用是:

  • LoginView:相当于MVP中的V,充当view的角色,只需要负责页面显示。
  • ILoginView:定义V中所有的UI动作协议,作为VLoginPresenter交互的桥梁。
  • LoginPresenter:将ILoginView的请求交给LoginInterator,将LoginInterator的处理结果返回ILoginView
  • LoginInterator:负责网络请求、数据处理等具体的业务。

这种方式各司其职,隔离性好。虽然好,但是我们却不能全部引用。如果工程页面非常多,针对每个页面,都需要额外创建三个类去处理他的交互逻辑,工程将会非常庞大,它更适合处理个别复杂的页面逻辑。在实际中,有非常多的页面是很简单的,所以怎么用一个最简单方便的MVP思路去组织我们的页面逻辑?

我们在前面的MVP讲解中提到,针对每个简单View,我们只创建一个Presenter与之对应负责处理基本数据,面对复杂页面,可以在UI层和Presenter之间设置中介者Mediator,将例如数据校验、组装在内的轻量级逻辑操作放在Mediator中;在Presenter和Model之间使用代理Proxy,用来分担Presenter的压力。结合上面的LoginInterator,便是LoginPresenter抽离出来的Proxy,将与Model的交互抽离出来交给LoginInterator处理,就是代理模式。如果View与Presenter之间的数据校验、逻辑操作比较负责,还可以抽离出LoginMediator,作为中介模式处理数据校验等逻辑,这个模式在这个AndroidMVP开源框架没有涉及到。

Presenter = Mediator中介+Presenter+Proxy代理

官方的architecture-samples中同样提供了一个MVP的示例,35.8k个star,给出了最全面的也是最为复杂的MVP架构设计。有四个角色:

  • AddEditTaskFragment:相当于MVP中的V,充当view的角色,只需要负责页面显示。
  • AddEditTaskContractContract是合同的意思,相当于Mediator中介,也就是说,ViewPresenter没有直接关系,两边只能与中介签署合同关系。
  • AddEditTaskPresenter:中间介,与Mediator中介签署合同,但是他却不能生产数据,他还要去找Proxy数据代理才能生产数据。
  • TasksRepositoryRepository是仓库的意思,也就是Presenter只能去仓库找人给数据,需要别人代理他产生数据,这就是Proxy代理模式。

这样的架构设计可以应对非常复杂的页面逻辑,层次也会很清晰。我们看到Demo示例中,每个页面都要多出四个职责类去处理,相比前面的AndroidMVP还要多一层中介角色。大部分应用的页面关系与逻辑其实是非常简单的,如果页面非常之多,这个架构将带来工程庞大化的风险。

我相信官方的设计是从最为复杂以及全面的角度,我们不能全部照抄,得根据自己的实际情况来适当选择角色。对于简单页面,从简化工程角度考虑,对于大多数的简单页面,如果只需要对每个View创建一个Presenter做数据处理。所以重点的工作就在Presenter中,怎么合理规划代码结构,让大家按照统一的规范约束,方便后期维护。

Presenter = Presenter + IView

上面给出的方案比较复杂,对于很多简单页面来讲完全套用会导致工程庞大。我们根据我们工程结构约束以及实际页面场景,对Presenter尽可能的优化又要防止臃肿,采取的第一步简化的方案:Presenter + IView,下面我们给出示例。

这种方案,Presenter给出View的所有业务接口,View负责实现这些接口,同时Presenter持有View的应用,正常情况下一个View对应一个Presenter

public class LoginPresenter {

    private ILoginView iLoginView;

    /**
     * 保存ILoginView对象,与View绑定关系
     */
    public LoginPresenter(ILoginView iLoginView){
        this.iLoginView = iLoginView;
    }

    /**
     * 初始化,填充上次输入的账号密码
     */
    public void init(){
        iLoginView.initAccountPassword("张三","8888");
    }

    /**
     * 登录,给登录的View使用
     */
    public void login(String name, String password){
        //...省略和网络以及数据库的逻辑,直接返回登录结果
        boolean success = false;
        if(success){
            iLoginView.loginSuccess();
        }else{
            iLoginView.passwordWrong();
        }
    }

    /* ***************************** LoginView的Iview***************************** */

    /**
     * 登录页面的所有业务逻辑接口
     */
    public interface ILoginView{
        //登录成功
        void loginSuccess();
        //密码错误
        void passwordWrong();
        //设置上次保存的账号和密码
        void initAccountPassword(String account, String name);
    }
}

View对应的登录页面如下:

public class Frag_xxx extends RootFrag implements LoginPresenter.ILoginView{

	private TextView tvAccount, tvPassword, tvLogin;//账号密码的View
	private LoginPresenter loginPresenter;//登录中间介
	
    @Override
    public void initViewFinish(View view) {
    	initPresenter();
        //初始化登陆事件
        tvLogin.setOnClickListener(v -> {
            loginPresenter.login("张三","888888");
        });
    }
    
	private void initPresenter(){
		//LoginView被Presenter持有
        loginPresenter = new LoginPresenter(this);
        //初始化
        loginPresenter.init();
	}
	
    /* ******************** LoginView的Iview的实现******************** */

	@Override
    public void loginSuccess() {
        //跳转下个页面
    }

    @Override
    public void passwordWrong() {
        //提示密码错误
    }

    @Override
    public void initAccountPassword(String account, String password) {
        tvAccount.setText(account);
        tvPassword.setText(password);
    }
}

这种模式下更具有面向对象思想,面向接口变成。在严格Presenter绑定View的情况下,基本无法复用Presenter。从技术上来讲,这种方式确实是非常符合设计思想的,不过从维护上来讲,就不一定了,主要从下面两个方面讲:

  • 接口的@Override与生命周期等的@Override混淆,后期维护的时候,一旦生命周期多了,理清代码逻辑将是大工程。
  • 随着页面的复杂度增加,将存在多个implements 的情况,这时@Override实现的接口将会混淆,如果不从命名上区分(往往命名无法控制规范),将难以维护。
  • 如果部分接口方法不需要了,要么删除Presenter的方法定义与View方法实现,要么不删除但是View还留着空白实现,这种情况下会造成臃肿。

Presenter = Presenter

面相对象的设计原则中,有一条接口隔离原则,怎么样才算是最小接口?有的页面业务简单,有的页面业务复杂,而且不同的业务差别比较大,所以我们这里的接口以最小的业务种类划分,一个接口代表一类业务。比如登录成功与密码错误,算是两个业务,统一用一个接口去定义,一个业务对应一个接口,接口有且仅有一个方法

我们这种定义不是从纯技术上去考虑,而是以后期维护为出发点。当业务比较多的时候,就会存在比较多的接口,如果按照原来的方式摆放,会显得很难看,不易维护。我们参考iOSDelegate回调的摆放方式,将所有回调接口放在代码的末尾,并用pragma mark标签去作区分,简单明了。

#import "ViewController.h"

@interface ViewController ()<MyDelegate1,MyDelegate2,MyDelegate3>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

#pragma mark - MyDelegate1代理方法
- (void)myDelegate2Fun{
    NSLog(@"MyDelegate1的代理方法");
}

#pragma mark - MyDelegate1代理方法
- (void)myDelegate2Fun{
    NSLog(@"MyDelegate2的代理方法");
}

#pragma mark - MyDelegate1代理方法
- (void)myDelegate3Fun{
    NSLog(@"MyDelegate3的代理方法");
}
@end

所以最终的LoginPresenter组织如下:

public class LoginPresenter {

    /**
     * 登录,给登录的View使用
     */
    public void init(){
        onInitAccountPasswordNext("张三","888888");
    }

    /**
     * 登录,给登录的View使用
     */
    public void login(String name, String password){
        //...省略网络或者数据库逻辑,直接墨迹结果
        boolean success = false;
        if(success){
            onLoginSuccessNext();
        }else{
            onPasswordWrongNext();
        }
    }


    /* ***************************** LoginSuccess ***************************** */

    private OnLoginSuccessListener onLoginSuccessListener;

    // 接口类 -> OnLoginSuccessListener
    public interface OnLoginSuccessListener {
        void onLoginSuccess();
    }

    // 对外暴露接口 -> setOnLoginSuccessListener
    public void setOnLoginSuccessListener(OnLoginSuccessListener onLoginSuccessListener) {
        this.onLoginSuccessListener = onLoginSuccessListener;
    }

    // 内部使用方法 -> LoginSuccessNext
    private void onLoginSuccessNext() {
        if (onLoginSuccessListener != null) {
            onLoginSuccessListener.onLoginSuccess();
        }
    }

    /* ***************************** PasswordWrong ***************************** */

    private OnPasswordWrongListener onPasswordWrongListener;

    // 接口类 -> OnPasswordWrongListener
    public interface OnPasswordWrongListener {
        void onPasswordWrong();
    }

    // 对外暴露接口 -> setOnPasswordWrongListener
    public void setOnPasswordWrongListener(OnPasswordWrongListener onPasswordWrongListener) {
        this.onPasswordWrongListener = onPasswordWrongListener;
    }

    // 内部使用方法 -> PasswordWrongNext
    private void onPasswordWrongNext() {
        if (onPasswordWrongListener != null) {
            onPasswordWrongListener.onPasswordWrong();
        }
    }

    /* ***************************** InitAccountPassword ***************************** */

    private OnInitAccountPasswordListener onInitAccountPasswordListener;

    // 接口类 -> OnInitAccountPasswordListener
    public interface OnInitAccountPasswordListener {
        void onInitAccountPassword(String account, String password);
    }

    // 对外暴露接口 -> setOnInitAccountPasswordListener
    public void setOnInitAccountPasswordListener(OnInitAccountPasswordListener onInitAccountPasswordListener) {
        this.onInitAccountPasswordListener = onInitAccountPasswordListener;
    }

    // 内部使用方法 -> InitAccountPasswordNext
    private void onInitAccountPasswordNext(String account, String password) {
        if (onInitAccountPasswordListener != null) {
            onInitAccountPasswordListener.onInitAccountPassword(account,password);
        }
    }
}

登录页面的使用如下:

public class Frag_xxx extends RootFrag {

	private TextView tvAccount, tvPassword, tvLogin;//账号密码的View
	private LoginPresenter loginPresenter;//登录中间介

    @Override
    public void initViewFinish(View view) {
    	//初始化Presenter
        initPresenter();
        //初始化页面事件
        tvLogin.setOnClickListener(v -> {
            loginPresenter.login("张三","888888");
        });
    }

	private void initPresenter(){
        loginPresenter = new LoginPresenter();
        //登录成功,在Presenetr中处理逻辑,UI层不负责逻辑处理,只负责显示、提示、跳转
        loginPresenter.setOnLoginSuccessListener(() -> {
            //登陆成功的跳转逻辑
        });
        //密码错误
        loginPresenter.setOnPasswordWrongListener(() -> {
            //负责吐司等提示逻辑
        });
        //初始化回调接口
        loginPresenter.setOnInitAccountPasswordListener((account, password) -> {
            tvAccount.setText(account);
            tvPassword.setText(password);
        });
        //页面初始化,填充上次保存的账号密码
        loginPresenter.init();
    }
}

按照这个Presenter约定,以"接口隔离原则"为基本切入点,从最小业务接口方面定义我们的接口颗粒度,方便后期维护。当然我们这里有个重要的命名规则,就是以setOnXXXListener为对外方法,以onXXXNext为对内调用,以OnXXXListener为接口定义。当然是以牺牲Presenter代码行数换来后期维护优点,有得有失,但是总体利大于弊。主要有以下优点:

  • Presenter虽然代码行数增加,但是从采用iOS代码块隔离方式以及将代码块放在结尾,反而显得更加直观,更加清晰,一目了然。
  • 因为Presenter定义接口颗粒度非常小,View中可以按需调用接口,后期不需要的时候可以直接删掉,非常灵活。
  • 不存在@Override混淆,View干净清晰,方便后期维护。

Presenter = Presenter + LifeCycle

我们设计完Presenter,会面临Presenter如何关联Fragment生命周期的问题,会引入相对复杂的一一关联关系。Lifecycle 是一个类,它持有关于组件(如 ActivityFragment)生命周期状态的信息,并且允许其他对象观察此状态。释放onCreate()onDestroy()等生命周期,简化View层的逻辑。

public class BasePresenter implements LifecycleObserver {

    public BaseHelper(Fragment fragment){
        if(fragment != null){
            //绑定生命周期
            fragment.getLifecycle().addObserver(this);
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    void onCreate(@NotNull LifecycleOwner owner){

    }

	...

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    void onDestroy(@NotNull LifecycleOwner owner){

    }

    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
    void onLifecycleChanged(@NotNull LifecycleOwner owner,@NotNull Lifecycle.Event event){
		
    }
}

这里我直接将我想要观察到Presenter的生命周期事件都列了出来,然后封装到BasePresenter 中,这样每一个BasePresenter 的子类都能感知到Activity容器对应的生命周期事件,并在子类重写的方法中,对应相应行为。

public class Homepresenter extends BasePresenter{

    public HomeHelper(Fragment fragment){
        super(fragment);
    }

    @Override
    void onDestroy(@NotNull LifecycleOwner owner){
        //部分资源释放需要生命周期,按需重写父类的生命周期
    }
}

onCreateonDestroy事件之外,Lifecycle一共提供了所有的生命周期事件,只要通过注解进行声明,就能够使LifecycleObserver观察到对应的生命周期事件。

总结Google官方或者其他一些全为开原工程给出了MVP的最佳实践,但是不难看出,这些设计模式都是为了兼容最为复杂的业务逻辑基础上设计出来的,我们并不是每个页面都复杂,以偏概全反而不好。从业务方面考虑以及从后期考虑,找到我们最优最简化的MVP实践,才更适合我们。

  • 1
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值