腾讯Bugly特约作者: 王金波
问题描述
做过Android开发的人都遇到过这样的问题:随着需求的变化,某些入口界面通常会出现 UI的增加、减少、内容变化、以及跳转界面发生变化等问题。每次发生变化都要手动修改代码,而入口界面通常具有未读信息提醒这样的“小红点”逻辑;一旦UI变化,“小红点”逻辑也要重新计算。如果不同的RD来维护这些代码,耦合性非常高,出错概率也很大。本文以自选股的个人页卡为例(界面如下图所示),并给出了一套方案来解决动态更新UI的问题以及更好的解决未读提醒的逻辑。
旧的方案(Phase out)
(1)对于UI动态变化的问题,通常结合远程控制来解决。以上图的“资产管理”为例,旧的解决方案会在XML写死全部的item,如:“港股交易”、“基金交易”和“精品理财”这三个item。然后根据后台传递过来的json解析出需要隐藏哪些item。点击不同的item会跳转到不同的activity(如下图所示),这部分跳转操作也是写死在代码中的。
这解决了一部分问题,但是如果需求新增了item,比如新增了“沪深交易”、“美股交易”,那就需要改动现有代码了。
(2)对于未读指示(小红点)功能,它的作用是,有未读信息来了,需要在UI上面显示一个小红点提醒用户。比如下图的,股友动态的头像提醒,资产管理的“NEW”提醒,系统设置的新版本提醒等。
旧的方案是定义了一个int型(32位),用它的每一位代表一个UI上的Item。比如好友动态是第1位,未读提醒是第2位… 小红点思想是哪个item有未读信息,则该int型对应的那一位就置1,否则为0。一旦某个item有未读提醒的改变,则将这个int型对应的位改变,异步写入SharedPreference中,同时利用观察者模式通知UI做更新,如下图所示:
上述做法总体来说最大的缺陷就是没有做到“开放-封闭”原则。面对扩展的时候,即添加一个item则不得不修改现有代码,需要在该int型中添加一位标志位,观察者模式也要注册新item。所以下面我会介绍另一种方案可以更好的解决该问题。
新的解决方法
(1) 数据抽象
首先进行数据的抽象,并将UI进行分组,如下图所示:
按照组合模式,将数据以树形结构组织起来,表现“整体/部分”层次结构,如下图所示。这样做的好处是,可以以一致的方式来处理个别对象以及对象组合。蓝色的表示节点,而绿色的表示叶节点。
组合模式的类图,如下所示:
对UI进行的数据抽象。无论是ListItem列表项,还是GridView Item的项,都采用了PersonalItem对象来表示,如下所示:
对于PersonalItem来说,有些没有意义的方法(如add()、remove()、getChild())就不实现,调用它们会抛出UnsupportedOperationException()异常。
(2) 完美解决未读提醒(小红点)的问题
关于计算小红点,PersonalGroup类利用组合+迭代器的模式,代码如下:
这里使用了迭代器,用它遍历所有PersonalComponent组件。遍历过程中可能遇到PersonalItem也可能遇到PersonalGroup。由于它们都是PersonalComponent,且实现了getUnreadIndicator()方法,那我们只需调用getUnreadIndicator()即可。
如上图所示,PersonalGroup中加载的是PersonalComponent,可能是PersonalItem也可能是PersonalGroup。组合模式的优点就是无视具体类型 — 获取出来的都是PersonalComponent,然后利用多态,调用具体类的getUnredIndicatorCount()方法。如果是PersonalGroup,则继续调用它的这个方法(与此方法一样,会开始另一个遍历);如果为PersonalItem,则说明遍历到了树形结构的末端(即叶节点),则进行如下处理:
如果getUnreadIndicator为true,则表示该PersonalComponent需要显示小红点。因此,利用上述组合+迭代方式,运用递归在根节点处进行一次调用即可。如下图所示,当计算出叶节点“A股大赛”有未读提醒,则它上级的groups也有未读提醒,一直统计到根节点。
getUnredIndicatorCount()是每一个item自己来决定自己是否需要展示小红点的方法。这就是将局部与整体解耦了。整体上面,需要计算小红点,至于如何计算则委托给具体类来实现。即面向对象中的将 “做什么” 与 “怎么做”分开。RD可以从中解放出来,不必关注整体实现,只需关注自己的实现即可。比如,需要在“资产管理”中添加“美股交易”,RD只需添加“美股交易”的内容即可。下一节会说明,这部分内容也由远程控制来代劳了,远程控制传递过来的Date与本地存储的Date比较,如果是新的Date值,则证明这个Item为“NEW”,则对应的小红点需要显示。
(3)远程控制动态更新UI
当远程控制发生变化时(5分钟主动发一次请求),通过解析远程控制接口返回的json串,生成PersonalItem对象的列表。其中每一项对应UI上面的一个Item。需要注意的是,这里还包含了一个URL,它是点击UI控件跳转的URL。以“资产管理”为例,它包含“沪深交易”、“基金交易”等子项。当点击任意一个子项的时候启动的是同一个Activity - WebviewActivity,它包含一个WebView控件。因为每个子项的跳转URL不一样,所以这个WebView load了不同的URL,即完成了跳转不同界面的问题。 然后按照上面描述的树形结构,把PersonalItem放到Groups中。如下图所示:
Model存储了待显示的数据结构。这份数据通过Parser的解析生成UI的内容。过程如下图所示:
Parser模块是一个递归函数,递归的对Model进行解析。并将解析出来的List Item、Grid Group、GridView Item加载各自的XML文件,在程序中动态的添加UI组件。其中onClick事件是在定义PersonalItem的时候已经写好了回调。例如,“资产管理”属于Grid Group,其子项“沪深交易”、“基金交易”等属于GridView Item。在上述“Build PersonalItem Objects”步骤中,已经定义了onClick方法,调用onClick方法跳转至WebViewActivity,这个Activity会加载不同GridView Item的URL,从而实现点击不同item跳转不同页面的目的。
Note:
对于ListItem元素,即上图的列表项(不是GridView元素),并没有实现远程更新的策略。因为它们跳转的逻辑是跳转到各自的Activity,是固定不变的;并且它们的文字描述、图标、是否隐藏均不需要后台来控制更新。故实际项目中,只对GridView内容作了远程控制动态更新UI机制的处理。
另外,在通过远程控制动态更新UI的过程中也遇到了一些坑,比如远程控制更新的时刻,恰好用户退出app,此时系统刚好销毁activity。那么在执行到上述Parser模块的inflateUI的时候就需要判断当前上下文是否为空,如果为空则直接退出。
结论与数据
本文通过将UI数据进行抽象,利用组合模式进行数据的构建。利用递归的方式将数据映射为UI。同时处理了点击事件。数据源则可以通过远程控制动态的更新,RD从中解放。另外,组合+迭代器的方式完美的解决了小红点的问题,遵循了“开放-封闭”原则,将“做什么”与“怎么做”分开。下图从数据的角度描述了改版前后 代码量、Bug量 以及 RD工作量的差异。
腾讯Bugly简介
Bugly是腾讯内部产品质量监控平台的外发版本,其主要功能是App发布以后,对用户侧发生的Crash以及卡顿现象进行监控并上报,让开发同学可以第一时间了解到App的质量情况,及时机型修改。目前腾讯内部所有的产品,均在使用其进行线上产品的崩溃监控。