天天看点

App设计:消息推送和界面路由跳转

app消息推送、显示通知栏,点击跳转页面是很一般的功能了,下面以个推为例演示push集成,消息处理模块及app内部路由模块的简单设计。

集成sdk步骤根据文档一步步做就行了,一般包括lib引入,<code>AndroidManifest.xml</code>配置,gradle配置,拷贝资源和java文件等。

需要注意的,自己应该做一层封装,因为像图片,统计,推送等第三方api,如果有替换升级等需求,那么封装一层来确保自己代码的更少变动也是蛮必要的。

服务端推送消息的操作是非UI操作,个推接入后在一个IntentService中收到透传消息(透明传输消息):

payload就是收到的push消息,一般是约定好的json文本。

下面是一个基本示例:

一般的推送都需要立即显示通知的,所以会有通知的信息。当然也可以是不带通知的推送。

这里payload里面携带了点击推送后的操作数据,type="page"表示此推送需要执行一个跳转。

path是跳转到的(以下路由表示相同含义)页面的路径——类似url那样的格式,抽象了具体界面。params包括了跳转相关参数,比如这里需要打开文章详情页,那么传递了文章id。

web中的url跳转机制非常值得借鉴。

程序设计中,有一种模式:命令模式,将操作和具体的执行分开。安卓系统中的输入事件的处理,Handler+Message机制等,都是类似的。

Msg用来抽象一个消息,而对应的有Handler来处理它。

这样的好处是命令被对象化,之后对它的处理可以利用多态等特性,命令的处理或许要经历多个阶段(Stage),这样可以动态组合不同的Handler完成对同一个Msg的处理。

如果仅仅是简单的switch+static method去实现的话,随着业务增加,是无法胜任变化的。如果有实现涉及到“消息处理”类似功能的话,不同消息不同处理的大前提,多重处理的需要,会让switch泛滥成灾的,而msg+handler仅需要一次switch选择合适的Handler,之后的处理是链式的,不会有再次switch的需要的。

可以思考下“消息+处理”这类功能的设计方案。

下面分PushMessage和PushHandler两个抽象,分别是推送消息和对应处理。

这里的思路借鉴了安卓中Handler的机制——Handler+Message这样的设计。

此外,源码ViewRootImpl、InputStage对输入事件的处理也可以借鉴。

类PushMessage其实就是个bean,它对后台推送的消息进行表示。

每一个PushHandler处理一个PushMessage。这里是一个基类:

handlePushMsg()用来供子类完成具体的消息处理。

这里假设业务功能上,需要一类推送是弹通知,并处理通知点击后的路由操作——界面跳转。

这里引入另一个模块——路由模块,路由模块完成界面跳转相关操作。

像Arouter这样的开源库就是做这类事情的——不论web还是移动app,都会碰到接收并响应界面跳转指令的功能。

接下来继续自己尝试实现路由功能。

因为路由模块和推送不是相关的——路由命令(或者称为消息)的发出不一定是推送,也可以是其它界面中的按钮等,知道路由模块和推送模块需要分别设计很重要。

有一部分推送是需要执行路由的,对这类推送的处理就是得到其对应的路由命令,之后交给路由模块去处理。

BaseRoutePushHandler重写handlePushMsg()完成routeMsg——路由命令的push消息的处理。getRouteMsg()供子类获取到路由命令的消息对象,之后交给RouterManager去处理。

路由模块实现app内不同界面之间的跳转导航。设计上,RouteMsg表示一个具体的路由命令,之后会有一个(或多个——如果对命令的处理是链式的话?)RouteHandler来处理此路由消息。

鉴于URL对不同web界面的定位导航优势,为系统中不同的跳转定义路由path是很不错的想法。

甚至可以定位到界面中的tab子界面,如果直接去关联Activity等,那么耦合非常严重。

RouteMsg设计上只用来表达路由命令,它包含路由path和额外参数。为了面向对象化,参数是有含义的强类型,而不是queryParams那样的基本类型key-value集合,要知道key的命名本身就是一种依赖,那么还不如定义key对应的java属性更直接些。

RouteMsg也是一个bean,当然可以跨进程,这里实现Parcelable当然更好,简单点就实现Serializable标记接口即可。

基类BaseRouteMsg定义如下:

其中getPath()方法要求每个具体的路由消息声明其对应的跳转路径。子类可以定义其它任意属性——可以被序列化即可。

作为示例,下面是文章详情界面的跳转路由消息:

对应每个RouteMsg对象需要有RouteHandler来处理它,这里引入路由表的概念——RouteMap,它定义了所有的path常量以及获取不同path对应RouteHandler的方法(工厂方法)。

getRouter(path)根据path返回处理它的RouteHandler,并且RouteMap定义了所有可能的路由path。BaseRouter就是处理某个path对应路由消息的Handler。

基类BaseRouter是抽象的路由消息处理器。将路由模块作为框架设计,需要尽可能使用抽象的东西,允许变更及扩展。

对于mRouteMsg可能更应该是构造函数参数,而且藐似不应该被setter篡改。这里为了可能的方便性(目前不知道是什么),决定还是作为普通的属性对待。

注意Context是android中的上帝对象,可以肯定导航操作需要它,但为了弱化它和RouteHandler的依赖关系(或许是生命周期)仅作为参数提供,而非字段。

方法canRoute(context)用来做导航操作的前置判断,因为路由可能涉及登录判断等环境问题,这个逻辑需要子类去重写,如果没特殊要求,这里默认返回true。

方法navigate(context)是具体的导航操作,如打开某个Activity。

上面分别介绍了推送和路由模块的大体设计,那么收到一个推送消息,弹出通知,用户点击通知后的跳转,这一系列操作是如何贯彻的呢?接下来就看看。

在sdk提供的IntentService.onReceiveMessageData()中收到透传消息,这里的代码是依赖服务器返回的数据格式的,即json和PushMessage对应,第一步将push消息转为java对象,接着交给PushManager去处理:

这里使用一个Manaher类来完成对PushMessage的一般处理逻辑。因为需求假定push都需要谈通知,并且通知点击后执行路由,那么先得到一个routeMsg,之后调用NotifiyManager.notifyRouteMsg()来发送通知。

通知以类似Intent的方式携带了之后的路由消息数据。

安卓中发送通知到通知栏是很简单的操作,需要注意的是:

使用NotificationCompat.Builder 来避免兼容问题。

建议使用<code>String tag</code>来区分不同的通知。

使用tag来发送通知的notify()方法如下:

因为id是一个int整数,很难做到对不同业务通知进行唯一区分。

使用tag,因为是一个可以组合的字符串,那么格式就比较灵活了,例如可以使用uri这种格式,或者其它任意你能够轻松用来区分不同业务模块不同通知的格式来产生tag作为通知的标识。

有关Notification的完整用法这里不去展开,为了能在点击通知之后做一些控制——比如判断用户是否登录等,可以让通知的点击行为是打开一个Service,而不是跳转到某个Activity。

这样的好处是不至于去修改Activity的代码来插入通知跳转的各种逻辑,当然必要的处理有时是必须的——比如Activity打开后清除对应通知。但这类工作可以做的更一般化,让Activity提供最少的逻辑,比如提供管理的跳转path,这样清除通知(或需要撤销的其它路由命令)的动作就可以框架去做了。这部分的功能目前不打算提供,但的确是一个需要考虑的必要功能。

下面的代码展示了点击通知启动Service的操作:

类RouteIntentService是继承IntentService的业务类,它响应所有来源(包括此处的通知)的路由命令。下面看它是如何工作的。

在RouteIntentService.java中:

从intent中获取到发送通知时设置的routeMsg,交给RouterManager去处理。

调用RouteMap.getRouter()获取到对应routeMsg的处理器——router。

router.canJump()用来对当前导航做前置判断,默认返回true。

router.navigate(context)执行具体的跳转逻辑。

作为示例,文章详情界面的路由器如下:

本文整理了实现“推送、通知、页面跳转”功能的一个简单设计。

Message+Handler模式是一个典型的编程模型。类似Task+Schedulers(异步任务+线程池)那样,体现一种数据和处理的分离思想。

如果后续有更多的关于推送、路由的要求,优先选择改进框架去满足一般需求。

面向抽象编程,不要直接对具体业务编程。

TODO:demo代码后续补上。

(本文使用Atom编写)