App设计:消息推送和界面路由跳转
概要
app消息推送、显示通知栏,点击跳转页面是很一般的功能了,下面以个推为例演示push集成,消息处理模块及app内部路由模块的简单设计。
推送
推送sdk集成
集成sdk步骤根据文档一步步做就行了,一般包括lib引入,AndroidManifest.xml
配置,gradle配置,拷贝资源和java文件等。
需要注意的,自己应该做一层封装,因为像图片,统计,推送等第三方api,如果有替换升级等需求,那么封装一层来确保自己代码的更少变动也是蛮必要的。
服务端推送消息的操作是非UI操作,个推接入后在一个IntentService中收到透传消息(透明传输消息):
@Override
public void onReceiveMessageData(Context context, GTTransmitMessage msg) {
try {
String payload = new String(msg.getPayload());
PushManager.handlePushMsg(context, payload);
} catch (Exception ex) {
// log it.
}
}
payload就是收到的push消息,一般是约定好的json文本。
下面是一个基本示例:
{
title:"通知标题",
content:"通知内容",
payload: {
type:"page",
extra:{
path:"/article/articleDetail",
params:{
"id":"10086"
}
}
}
}
一般的推送都需要立即显示通知的,所以会有通知的信息。当然也可以是不带通知的推送。
这里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
类PushMessage其实就是个bean,它对后台推送的消息进行表示。
class PushMessage implements Serializable {
private String title;
private String content;
private Payload payload;
...
}
PushHandler
每一个PushHandler处理一个PushMessage。这里是一个基类:
/**
* PushMsgHandler基类
* ,PushMsgHandler用来处理“某一个”PushMessage
*/
public abstract class BasePushMsgHandler {
protected PushMessage mMessage;
public BasePushMsgHandler(PushMessage message) {
mMessage = message;
}
public abstract void handlePushMsg();
}
handlePushMsg()用来供子类完成具体的消息处理。
这里假设业务功能上,需要一类推送是弹通知,并处理通知点击后的路由操作——界面跳转。
这里引入另一个模块——路由模块,路由模块完成界面跳转相关操作。
像Arouter这样的开源库就是做这类事情的——不论web还是移动app,都会碰到接收并响应界面跳转指令的功能。
接下来继续自己尝试实现路由功能。
因为路由模块和推送不是相关的——路由命令(或者称为消息)的发出不一定是推送,也可以是其它界面中的按钮等,知道路由模块和推送模块需要分别设计很重要。
有一部分推送是需要执行路由的,对这类推送的处理就是得到其对应的路由命令,之后交给路由模块去处理。
public abstract class BaseRoutePushHandler extends BasePushMsgHandler {
public BaseRoutePushHandler(PushMessage message) {
super(message);
}
@Override
public void handlePushMsg(Context context) {
BaseRouteMsg msg = getRouteMsg();
if (msg != null) {
RouterManager.navigate(context, msg);
}
}
public abstract BaseRouteMsg getRouteMsg();
}
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定义如下:
public abstract class BaseRouteMsg implements Serializable {
private static int mIdSeed;
static {
// 设置mIdSeed初始值:
// 允许0.1s一个的间隔,不会有超过8*100天的msg还没被处理
long stamp = System.currentTimeMillis() / 100;
mIdSeed = (int) (stamp % (8 * 24 * 3600 * 1000));
}
private final int mMsgId = mIdSeed++;
/**
* 获取路由对应的path
* @return route path
* @see RouteMap
*/
public abstract String getPath();
/**
* 消息编号,递增唯一(方便跨进程)。
*/
public final int getMsgId() {
return mMsgId;
}
}
其中getPath()方法要求每个具体的路由消息声明其对应的跳转路径。子类可以定义其它任意属性——可以被序列化即可。
作为示例,下面是文章详情界面的跳转路由消息:
public class ArticleDetailMsg extends BaseRouteMsg {
private int mArticleId;
@Override
public String getPath() {
return RouteMap.PATH_ARTICLE_DETAIL;
}
public int getArticleId() {
return mArticleId;
}
public void setArticleId(int articleId) {
this.mArticleId = articleId;
}
}
RouteMap
对应每个RouteMsg对象需要有RouteHandler来处理它,这里引入路由表的概念——RouteMap,它定义了所有的path常量以及获取不同path对应RouteHandler的方法(工厂方法)。
public final class RouteMap {
public static final String PATH_ARTICLE_DETAIL = "articleDetail";
public static BaseRouter getRouter(String path) {
switch (path) {
case RouteMap.PATH_ARTICLE_DETAIL:
return new ArticleDetailRouter();
default:
return null;
}
}
}
getRouter(path)根据path返回处理它的RouteHandler,并且RouteMap定义了所有可能的路由path。BaseRouter就是处理某个path对应路由消息的Handler。
BaseRouter
基类BaseRouter是抽象的路由消息处理器。将路由模块作为框架设计,需要尽可能使用抽象的东西,允许变更及扩展。
public abstract class BaseRouter<T> {
protected T mRouteMsg;
/**
* 路由操作的前置判断
* @return 是否继续前往目标界面
*/
public boolean canRoute(Context context) {
return true;
}
/**
* 执行导航到目标界面
*
* @return 导航成功?
*/
public abstract boolean navigate(Context context);
public void setRouteMsg(T msg) {
mRouteMsg = msg;
}
}
对于mRouteMsg可能更应该是构造函数参数,而且藐似不应该被setter篡改。这里为了可能的方便性(目前不知道是什么),决定还是作为普通的属性对待。
注意Context是android中的上帝对象,可以肯定导航操作需要它,但为了弱化它和RouteHandler的依赖关系(或许是生命周期)仅作为参数提供,而非字段。
方法canRoute(context)用来做导航操作的前置判断,因为路由可能涉及登录判断等环境问题,这个逻辑需要子类去重写,如果没特殊要求,这里默认返回true。
方法navigate(context)是具体的导航操作,如打开某个Activity。
推送-通知-路由处理流程
上面分别介绍了推送和路由模块的大体设计,那么收到一个推送消息,弹出通知,用户点击通知后的跳转,这一系列操作是如何贯彻的呢?接下来就看看。
响应推送消息
在sdk提供的IntentService.onReceiveMessageData()中收到透传消息,这里的代码是依赖服务器返回的数据格式的,即json和PushMessage对应,第一步将push消息转为java对象,接着交给PushManager去处理:
// 在PushIntentService.java中,这是sdk提供的接收推送消息的地方
public void onReceiveMessageData(Context context, GTTransmitMessage msg) {
try {
String payload = new String(msg.getPayload());
PushMessage message = PushMessage.build(payload);
PushManager.handlePushMsg(context, message);
} catch (Exception ex) {
}
}
// PushManager.handlePushMsg()
public static void handlePushMsg(Context context, PushMessage message) throws Exception {
BasePushMsgHandler pushHandler = PushHandlerManager.getPushHandler(message);
if (pushHandler != null) {
BaseRouteMsg routeMsg = pushHandler.getRouteMsg();
if (routeMsg != null) {
NotifiyManager.notifyRouteMsg(context, message.getTitle()
, message.getContent(), routeMsg);
}
}
}
这里使用一个Manaher类来完成对PushMessage的一般处理逻辑。因为需求假定push都需要谈通知,并且通知点击后执行路由,那么先得到一个routeMsg,之后调用NotifiyManager.notifyRouteMsg()来发送通知。
通知以类似Intent的方式携带了之后的路由消息数据。
弹出通知
安卓中发送通知到通知栏是很简单的操作,需要注意的是:
- 使用NotificationCompat.Builder 来避免兼容问题。
- 建议使用
String tag
来区分不同的通知。
使用tag区分通知
使用tag来发送通知的notify()方法如下:
/**
* Post a notification to be shown in the status bar. If a notification with
* the same tag and id has already been posted by your application and has not yet been
* canceled, it will be replaced by the updated information.
*
* @param tag A string identifier for this notification. May be {@code null}.
* @param id An identifier for this notification. The pair (tag, id) must be unique
* within your application.
* @param notification A {@link Notification} object describing what to
* show the user. Must not be null.
*/
public void notify(String tag, int id, Notification notification)
因为id是一个int整数,很难做到对不同业务通知进行唯一区分。
使用tag,因为是一个可以组合的字符串,那么格式就比较灵活了,例如可以使用uri这种格式,或者其它任意你能够轻松用来区分不同业务模块不同通知的格式来产生tag作为通知的标识。
通知点击效果
有关Notification的完整用法这里不去展开,为了能在点击通知之后做一些控制——比如判断用户是否登录等,可以让通知的点击行为是打开一个Service,而不是跳转到某个Activity。
这样的好处是不至于去修改Activity的代码来插入通知跳转的各种逻辑,当然必要的处理有时是必须的——比如Activity打开后清除对应通知。但这类工作可以做的更一般化,让Activity提供最少的逻辑,比如提供管理的跳转path,这样清除通知(或需要撤销的其它路由命令)的动作就可以框架去做了。这部分的功能目前不打算提供,但的确是一个需要考虑的必要功能。
下面的代码展示了点击通知启动Service的操作:
private static void sendRouteNotification(Context context, String title, String content,
String notificationTag, BaseRouteMsg msg) {
Intent startIntent = RouteIntentService.getIntentOfActionDoRoute(context, msg);
PendingIntent pendingIntent = PendingIntent.getService(context, DEFAULT_SERVICE_REQUEST_CODE,
startIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(content)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
NotificationManager notifyMgr =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notifyMgr.notify(notificationTag, NOTIFICATION_ID, builder.build());
}
类RouteIntentService是继承IntentService的业务类,它响应所有来源(包括此处的通知)的路由命令。下面看它是如何工作的。
响应通知点击
在RouteIntentService.java中:
// RouteIntentService.onHandleIntent()
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_DO_ROUTE.equals(action)) {
BaseRouteMsg routeMsg = (BaseRouteMsg) intent.getSerializableExtra(EXTRA_ROUTE_MSG);
handleActionDoRoute(routeMsg);
}
}
}
// RouteIntentService.onHandleIntent()
/**
* 处理路由跳转命令
* @param routeMsg
*/
private void handleActionDoRoute(BaseRouteMsg routeMsg) {
boolean jumpDone = false;
try {
if (routeMsg != null) {
jumpDone = RouterManager.navigate(this, routeMsg);
}
} catch (Exception e) {
}
if (!jumpDone) {
RouterManager.openApp(this);
}
}
从intent中获取到发送通知时设置的routeMsg,交给RouterManager去处理。
// 在RouterManager.navigate()
/**
* 导航到目标界面
*
* @param msg 路由信息
* @return 是否完成跳转
*/
public static boolean navigate(Context context, BaseRouteMsg msg) {
BaseRouter router = RouteMap.getRouter(msg.getPath());
if (router == null || !router.canJump(context)) {
return false;
}
router.setRouteMsg(msg);
return router.navigate(context);
}
调用RouteMap.getRouter()获取到对应routeMsg的处理器——router。
router.canJump()用来对当前导航做前置判断,默认返回true。
router.navigate(context)执行具体的跳转逻辑。
作为示例,文章详情界面的路由器如下:
public class ArticleDetailRouter extends BaseRouter<ArticleDetailMsg> {
@Override
public boolean navigate(Context context) {
if (mRouteMsg == null) {
return false;
}
ArticleDetailActivity.launchActivity(context, mRouteMsg.getArticleId());
return true;
}
}
小结
本文整理了实现“推送、通知、页面跳转”功能的一个简单设计。
Message+Handler模式是一个典型的编程模型。类似Task+Schedulers(异步任务+线程池)那样,体现一种数据和处理的分离思想。
如果后续有更多的关于推送、路由的要求,优先选择改进框架去满足一般需求。
面向抽象编程,不要直接对具体业务编程。
TODO:demo代码后续补上。
(本文使用Atom编写)