App設計:消息推送和界面路由跳轉

概要

app消息推送、顯示通知欄,點擊跳轉頁面是很通常的功能了,下面以個推爲例演示push集成,消息處理模塊及app內部路由模塊的簡單設計。java

推送

推送sdk集成

集成sdk步驟根據文檔一步步作就好了,通常包括lib引入,AndroidManifest.xml配置,gradle配置,拷貝資源和java文件等。
須要注意的,本身應該作一層封裝,由於像圖片,統計,推送等第三方api,若是有替換升級等需求,那麼封裝一層來確保本身代碼的更少變更也是蠻必要的。android

服務端推送消息的操做是非UI操做,個推接入後在一個IntentService中收到透傳消息(透明傳輸消息):web

@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。json

web中的url跳起色制很是值得借鑑。api

消息&處理

程序設計中,有一種模式:命令模式,將操做和具體的執行分開。安卓系統中的輸入事件的處理,Handler+Message機制等,都是相似的。
Msg用來抽象一個消息,而對應的有Handler來處理它。
這樣的好處是命令被對象化,以後對它的處理能夠利用多態等特性,命令的處理或許要經歷多個階段(Stage),這樣能夠動態組合不一樣的Handler完成對同一個Msg的處理。服務器

若是僅僅是簡單的switch+static method去實現的話,隨着業務增長,是沒法勝任變化的。若是有實現涉及到「消息處理」相似功能的話,不一樣消息不一樣處理的大前提,多重處理的須要,會讓switch氾濫成災的,而msg+handler僅須要一次switch選擇合適的Handler,以後的處理是鏈式的,不會有再次switch的須要的。app

推送處理

能夠思考下「消息+處理」這類功能的設計方案。
下面分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的方式攜帶了以後的路由消息數據。

彈出通知

安卓中發送通知到通知欄是很簡單的操做,須要注意的是:

  1. 使用NotificationCompat.Builder 來避免兼容問題。
  2. 建議使用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編寫)

相關文章
相關標籤/搜索