動態導航總線 Phoenix – URL Router
在我們 Android 開發過程中,在做頁面跳轉的時候,一般情況下都是寫死了代碼邏輯,比如 startActivity(new Intent(context, SomeActivity.class)) 或者使用 scheme URL 方式隱性 Intent 跳轉,無論如何,一旦我們寫完了代碼發布出去后,便無法更改跳轉邏輯了,而且傳統的 Class 跳轉將強依賴目標類文件,造成了模塊化開發的困難。
如果這時候某個頁面出現了巨大的 bug,我們無法在不更新程序的情況下,動態將它轉向到一個專門的 error 頁面或者某個臨時替代的 H5 Web 頁面。
即 A -> B 是一個既定的關系,為了達到動態化,必然需要對整個 App 的導航進行統一處理,經過路由中心分發,這就是 URL Router 要做的事情。
Phoenix-URLRouter 便是這樣一個 Android 平臺上的 URL Router,實現了路由規則的動態可配置性,AOP,以及方便的參數、數據傳遞。
特性
- 實現 AOP (面向切面編程), 能夠在 URI 變換前后插入攔截器( Interceptor )
- 攔截器將能夠決定 Chain 是否繼續、中止或轉向
- 能夠隨意更改諸如 drakeet.app/home 的映射:
- 使用諸如 drakeet.app/home 能打開 Home 頁
- 當 drakeet.app/home 不存在映射時, 不做任何響應, 但可被攔截
- 使用諸如 http://drakeet.app/home 能打開原生 Home 頁, 當映射不存在的時候, 能夠使用 web 頁打開 URL
- 使用諸如 cn://drakeet.app/home 能打開 Home 頁
- 使用諸如 cn://drakeet.app/home 能映射到 Error 頁, 或做錯誤處理
- 使用諸如 https://drakeet.app/home?user_id=drakeet 能跳轉到 http://xxx.com/xxx?user_id_drakeet
- 能夠攜帶 Parcelable / Serializable / Bundle 數據
- 能夠設置出錯回調
- 支持 class 和 Scheme URL 作為 target
- 完備的 Espresso / 單元測試
思考
- 攔截器是否需要支持修改、新增攜帶的數據(區別于參數)
- 能夠設置跳轉白名單(不必要,跳轉的目標原本就是在被限定的范圍)
- 是否要支持服務端插入新參數, 將決定:
- 使用諸如 cn://drakeet.app/home 能變成 cn://drakeet.app/home?show_dialog=xxx 從而能擊中預設的攔截規則, 彈出對話框 Dialog (考慮到純粹性和安全性,已放棄)
使用
全局初始化
public class App extends Application {
@Override public void onCreate() {
super.onCreate();
CrashWoodpecker.flyTo(this);
Map<String, String> url2pageMap = new HashMap<>();
url2pageMap.put("m.drakeet.me/home", "home");
url2pageMap.put("m.drakeet.me/web", "web");
PageBinder.bind("home", "cn://m.drakeet.me/home");
PageBinder.bind("web", "cp://m.drakeet.me/web");
Phoenix.install(this)
.addRequestInterceptor(new LogInterceptor("Request"))
.addTargetInterceptor(new LogInterceptor("Target"))
.addHttpUrlNotFoundObserver(new HttpUrlObserver())
.addErrorObserver(new OnErrorObserver());
Phoenix.apply(this, url2pageMap);
}
}
- addRequestInterceptor 插入請求攔截器
- addTargetInterceptor 插入結果攔截器
- addHttpUrlNotFoundObserver 對于 HTTP URL 如果沒有找到映射目標,則交給此 Observer 對象
- Phoenix.apply 動態配置路由規則接口
攜帶參數跳轉
m.drakeet.me/home 映射 cp://m.drakeet.com/home scheme
public void onStartHomeWithParams(View view) {
Phoenix.navigation()
.navigate(this, "m.drakeet.me/home")
.appendQueryParameter("tag", "just_params")
.appendQueryParameter("tab", "delivery")
.appendQueryParameter("user_id", "drakeet")
.start();
}
攜帶參數 & 數據跳轉
m.drakeet.me/home 映射 cp://m.drakeet.com/home scheme
public void onStartHomeWithExtraDataAndParams(View view) {
/* A java bean implements Parcelable */
Mail mail = new Mail();
mail.name = "A gift from drakeet";
mail.from = "drakeet";
mail.to = "Xiaoai";
Phoenix.navigation()
.navigate(this, "m.drakeet.me/home")
.appendQueryParameter("tab", "delivery")
.appendQueryParameter("user_id", "drakeet")
.thenExtra() // 轉向到數據配置
.putParcelable(KEY_MAIL, mail)
.start();
}
映射到某個 Activity.class – 已廢棄
由于考慮到攔截器統一性參數類型, 并結合 Class 的必要性和其帶來的弊端, 如對于 Class 文件的耦合, 廢棄了此接口
https://m.drakeet.me/home_by_class 映射 HomeActivity.class
public void onStartClassHome(View view) {
Phoenix.navigation()
.navigate(this, "https://m.drakeet.me/home_by_class")
.appendQueryParameter("tag", "just_params")
.appendQueryParameter("tab", "delivery")
.appendQueryParameter("user_id", "drakeet")
.start();
}
示例: 消息中心
/**
* http://m.drakeet.com/order_detail -> m.drakeet.com/order_detail
* -> cp://drakeet.sdk/target -> TargetActivity(order_detail)
*/
public void onMessageToDetailClick(View view) {
Order order = new Order(123456);
Bundle bundle = new Bundle();
data.putString("xxxKey", "xxxValue");
Phoenix.navigation()
.navigate(this, "http://m.drakeet.com/home")
.appendQueryParameter("xxx", "sss")
.appendQueryParameter("user_id", "drakeet")
.thenExtra()
.putSerializable("key_order", order)
.putExtras(bundle)
.start();
}
/**
* https://m.drakeet.com/post -> m.drakeet.com/post
* -> cp://drakeet.sdk/target -> TargetActivity(punish)
*/
public void onMessageToPunishClick(View view) {
Phoenix.navigation()
.navigate(this, "https://m.drakeet.com/post")
.appendQueryParameter("user_id", "xiaoai")
.thenExtra()
.start();
}
Phoenix APIs
public interface Navigation {
interface Encode {
Encode appendQueryParameter(String key, String value);
Extra thenExtra();
void start();
}
interface Extra {
Extra putParcelable(String key, Parcelable value);
Extra putSerializable(String key, Serializable value);
Extra putExtras(Bundle bundle);
void start();
}
Encode navigate(Context context, String baseUrl);
Encode navigateWithFlag(Context context, String baseUrl, int flag);
void start();
}
后話
這個 URL Router 目前大致思想和設計就是這樣了,一開始也只是聽說要做這么一個東西,沒有任何借鑒和參考,完全一人花費了兩三天獨立設計和開發完成,暫沒有開源,目前它被用于菜鳥 SDK 中 native 頁面和 Weex 的導航中轉。
來自:https://drakeet.me/phoenix-url-router