概要
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編寫)
文章列表