本來只是想把本身的思路和想法給你們分享一下,沒想到有這麼多人的關注和喜歡,實在是有點受寵苦驚。或許是思路太長,給某些人形成了誤解,在此作一個說明。(若是尚未看過文章,能夠直接從下面分隔線開始)java
這裏講的跟網絡攔截沒有什麼關係。git
這裏講的不只僅是登陸跳轉,而是由登陸跳轉引出的一個特殊場景需求。就是有前置條件下延遲任務處理的問題。程序員
由於本人沒有找到更好的方案,因此作了這個設計。但願有更好方案的朋友留言提出。github
另外收到部分朋友的反饋,任務邏輯處理的不夠簡潔。特此修改了第二版的實現。核心代碼以下bash
ps:當時設計的一個初衷,是考慮到前置條件中可能會嵌套目標任務。可是如今想了好久,仍然沒有想到可能的業務場景。既然技術是爲業務存在,因此就取消了嵌套任務。若是有朋友有這樣的場景,請告訴我。網絡
/**
* Created by jinyabo on 13/12/2017.
*
* 若是CallUnit驗證模型中沒有嵌套的驗證模型,則能夠直接使用SingleCall便可
*/
public class SingleCall {
CallUnit callUnit = new CallUnit();
public SingleCall addAction(Action action){
clear();
callUnit.setAction(action);
return this;
}
public SingleCall addValid(Valid valid){
//只添加無效的,驗證不經過的。
if(valid.check()){
return this;
}
callUnit.addValid(valid);
return this;
}
public void doCall(){
//若是上一條valid難沒有經過,就直接返回
if(callUnit.getLastValid() != null && !callUnit.getLastValid().check() ){
return;
}
//執行action
if(callUnit.getValidQueue().size() == 0 && callUnit.getAction() != null){
callUnit.getAction().call();
//清空
clear();
}else{
//執行驗證。
Valid valid = callUnit.getValidQueue().poll();
callUnit.setLastValid(valid);
valid.doValid();
}
}
public void clear(){
callUnit.getValidQueue().clear();
callUnit.setAction(null);
callUnit.setLastValid(null);
}
// 單一全局訪問點
public static SingleCall getInstance() {
return SingletonHolder.mInstance;
}
// 靜態內部類,第一次加載Singleton類時不會初始化mInstance,
// 當調用getInstance()時纔會初始化
private static class SingletonHolder {
private static SingleCall mInstance = new SingleCall();
}
}
複製代碼
另外筆者本人也根據本身平時的業務需求,總結了以下幾種應用場景。框架
這裏總結下,這樣作的好處。ide
一、徹底支持上面全部的情形。不用作特殊判斷。post
二、圖中黃色區域,都在主界面所在的上下文中執行。邏輯就在當前界面,不會到無關界面中處理。作到了職責清晰。ui
三、調用起來更加簡單。
若是介紹不清楚,請直接看代碼。真的是比較簡單的。
----------------------------這是分隔線--------------------------
項目中常常有遇到一個典型的需求,就是在用戶在須要進入A界面的時候,須要先判斷用戶是否登陸,若是沒有登陸,則須要先進入登陸界面,若是登陸成功了,再直接跳轉到A界面。
因此這裏有兩個需求: 一、自動跳轉到登陸界面 二、登陸成功後再自動跳轉到目標A界面
若是咱們直接判斷用戶有沒有登陸,提醒用戶登陸。也沒有讓用戶登陸成功後再直接跳轉到目標界面,這樣的用戶體驗恐怕是不能知足一個高逼格程序員的要求。那麼,咱們來思考下,如何才能更加優雅的完成這個工做呢?
固然,在開始以前,咱們能夠先了解下其餘人都是怎麼作的,畢竟咱們能夠站在巨人的肩膀上才能看得更遠。
首先咱們第一個想到的解決方式,就是攔截器。若是咱們在進入A界面的時候,能夠在操做以前加入一個攔截器的話,豈不是能夠作到在進入A界面前的判斷呢?
A、 Android攔截器 (能夠點擊查看)
此方案經過註解。在進入目標界面A時,判斷是否有指定的攔截器,若是有,則檢驗是否知足攔截器要求,不知足,則執行攔截器的處理,處理完成後,經過onActivityResult最後觸發invoke的回調方法。
此方案和咱們需求略有不一樣,那麼說下此方案存在的缺點: 一、用了繼承的方式,來插入invoke的回調方法。因爲java的單繼承的特性,若是工程中已經有基類的狀況,調整起來比較麻煩。侵入性過高。
二、此方案中,在沒有登陸的狀況下,其實已經進入了目標A頁面。相應的初始化都已經執行了。若是沒有登陸成功,這樣工做實際上是白作了。若是目標A界面要登陸才能進入的話,此方案不符合要求的。
B、咱們直接使用路由框架,參考下阿里的ARouter方案,能夠看到,咱們能夠在固定路由上面插入攔截器。這裏有一篇文章介紹 阿里ARouter攔截器使用及源碼解析
看了文章後,發現攔截器實現的很是優雅,可是依然不是咱們想要的。由於這個攔截器執行完後,立刻會執行目標方法。中間並不會等待。因此咱們根本沒有辦法去執行咱們的登陸操做。 因此pass了。
咱們再回過頭來思考,攔截器彷佛並不能直接完成咱們的需求,由於咱們須要插入一個驗證行爲後(例如進入登陸界面),還要執行相應的操做後,保證這個驗證行爲經過後,才能真正進入到咱們的目標界面。
其實若是咱們只是單純的完成這個功能的話,可能你們最容易想到的就是,在進入登陸界面的時候,在intent中裝載一個目標target的intent.若是登陸成功了,就判斷是否有目標target,若是有,就跳轉到目標target.
Intent intent = new Intent(this,LoginActivity.class);
Intent target = new Intent(this,OrderDetailActivity.class);
intent.putExtra("target",target);
startActivity(intent);
複製代碼
這種方式作起來很是直接,也可理解,可是最明顯的問題就是,會致使登陸界面多了不少與本身無關的業務判斷。那咱們繼續google看看,有沒有相似的作法,而且實現優雅一點的呢?
Android 登陸判斷器,登陸成功後幫你準確跳轉到目標activity 這篇的訪問量比較大,彷佛是個比較靠譜的方法。咱們來大概分析下它的作法。
public static void interceptor(Context ctx, String target, Bundle bundle, Intent intent) {
if (target != null && target.length() > 0) {
LoginCarrier invoker = new LoginCarrier(target, bundle);
if (getLogin()) {
invoker.invoke(ctx);
} else {
if (intent == null) {
intent = new Intent(ctx, LoginActivity.class);
}
login(ctx, invoker, intent);
}
} else {
Toast.makeText(ctx, "沒有activity能夠跳轉", 300).show();
}
}
private static void login(Context context, LoginCarrier invoker, Intent intent) {
intent.putExtra(mINVOKER, invoker);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(intent);
}
複製代碼
咱們看上面的核心代碼就是,封裝一個LoginCarrier。若是沒有登陸,則把這個LoginCarrier傳入到登陸界面。登陸成功後,觸發invoke()方法。本質上和咱們上面的想法差很少。
看完以後,仍是以爲實現上不夠完美,總以爲有些缺點。例如
一、在登陸界面仍是侵入了過多的邏輯(這彷佛不可避免,可是否能夠簡潔些呢)
二、擴展性比較差。比方說我要購買某個禮品,須要登陸,而後再跳轉到充值界面充值完成後再回來。
那到底有沒有更好的實現方案呢,谷歌後,發現暫時沒有找到可靠的方案了,因此說靠天靠地,不如靠本身,既然找不到合適的方案,那就好好思考下,本身動手來幹了。
首先,咱們再回過頭考慮咱們的需求,咱們須要執行一個目標方法。可是目標方法須要一個前置的條件知足才能執行,而且這個前置條件可能不僅一個,還有就是這個前置條件並非立刻就能完成的。
那咱們根據需求抽象出來的數據模型應該是。
public class CallUnit {
//目標行爲
private Action action;
//先進先出驗證模型
private Queue<Valid> validQueue = new ArrayDeque<>();
//上一個執行的valid
private Valid lastValid;
}
複製代碼
那麼目標行爲action就是一個執行體。負責執行目標方法。
public interface Action {
void call();
}
複製代碼
驗證操做validQueue保存一個驗證隊列,Valid的驗證模型是
public interface Valid {
/**
* 是否知足檢驗器的要求,若是不知足的話,則執行doValid()方法。若是知足,則執行目標action.call
* @return
*/
boolean check();
//去執行驗證前置行爲,例如跳轉到登陸界面。(但並未完成驗證。)
void doValid();
}
複製代碼
那麼整個邏輯用一幅圖表達出來,會比較清楚。
接下來根據圖,來說解代碼實現。
第一步,咱們須要構造一個CallUnit單元。例如,咱們須要跳轉到折扣界面,前置是咱們必需要登陸,而且要有折扣碼。
因此這裏,咱們有兩個驗證模型,一個是登陸,一個是拿到折扣。
public class DiscountValid implements Valid {
private Context context;
public DiscountValid(Context context) {
this.context = context;
}
/**
*
* @return
*/
@Override
public boolean check() {
return UserConfigCache.isDiscount(context);
}
/**
* if check() return false. then doValid was called
*/
@Override
public void doValid() {
DiscountActivity.start((Activity) context);
}
}
public class LoginValid implements Valid {
private Context context;
public LoginValid(Context context) {
this.context = context;
}
/**
* check whether it login in or not
* @return
*/
@Override
public boolean check() {
return UserConfigCache.isLogin(context);
}
/**
* if check() return false. then doValid was called
*/
@Override
public void doValid() {
LoginActivity.start((Activity) context);
}
}
複製代碼
而後咱們須要構造一個執行體。直接在當前的Activity裏面實現Action接口便可。例如咱們在MainActivity中實現。
@Override
public void call() {
//這是咱們的目標行爲
OrderDetailActivity.startActivity(MainActivity.this, "1234");
}
複製代碼
接下來,咱們就能夠構造一個CallUnit對象並進行執行了。
CallUnit.newInstance(MainActivity.this)
.addValid(new LoginValid(MainActivity.this))
.addValid(new DiscountValid(MainActivity.this))
.doCall();
複製代碼
咱們來看看doCall到底作了什麼?
public void doCall(){
ActionManager.instance().postCallUnit(this);
}
複製代碼
發現,咱們是經過ActionManager的單例調用了postCallUnit().咱們看下這個單例有啥做用
public class ActionManager {
static ActionManager instance = new ActionManager();
public static ActionManager instance() {
return instance;
}
Stack<CallUnit> delaysActions = new Stack<>();
....
}
複製代碼
這個單例維護了一個CallUnit的堆棧,表示咱們支持一個目標行爲裏面再嵌入一個目標行爲。可是這個需求恐怕不多會遇到。可是設計上是支持的。
咱們再回過頭看看,postCallUnit()到底作了啥?
/**
* 根據條件判斷,是否要執行一個action
*
* @param callUnit
*/
public void postCallUnit(CallUnit callUnit) {
//清除全部的actions
delaysActions.clear();
//執行check
callUnit.check();
//若是所有知足,則直接跳轉目標方法
if (callUnit.getValidQueue().size() == 0) {
callUnit.getAction().call();
} else {
//加入到延遲執行體中來
delaysActions.push(callUnit);
Valid valid = callUnit.getValidQueue().peek();
callUnit.setLastValid(valid);
//是否會有後置任務
valid.doValid();
}
}
複製代碼
備註很是清楚,就是判斷是否驗證條件都知足,若是知足,則直接執行目標方法,若是不知足,則執行doValid方法。而且保存當前valid的引用,以便後面驗證valid是否知足條件。若是不知足,是不容許再執行下一輪的驗證。
到這裏,咱們知道,咱們已經觸發了執行體,並順利進入了登陸驗證的執行體。由於登陸這個操做須要用戶手動觸發完成,咱們只是引導用戶到了登陸界面(固然登陸操做也能夠代碼自動完成,那就沒有必要跳頁面了),因爲咱們由於等待用戶的輸入,咱們的驗證模型就在這裏停下來了,若是登陸成功了,咱們才須要讓整個驗證模型再運轉起來了,因此驗證後,永遠少不了手動開啓驗證模型。
例如咱們在登陸成功後,須要調用方法CallUnit.reCall():
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(LoginActivity.this,"登陸成功",Toast.LENGTH_SHORT).show();
UserConfigCache.setLogin(LoginActivity.this, true);
//這裏執行延遲的action方法。
CallUnit.reCall();
finish();
}
});
複製代碼
咱們看看CallUnit.reCall()的執行方法
public static void reCall(){
ActionManager.instance().checkValid();
}
public void checkValid() {
if (delaysActions.size() > 0) {
CallUnit callUnit = delaysActions.peek();
if (callUnit.getLastValid().check() == false) {
throw new ValidException(String.format("you must pass through the %s,and then reCall()", callUnit.getLastValid().getClass().toString()));
}
if (callUnit != null) {
Queue<Valid> validQueue = callUnit.getValidQueue();
validQueue.remove(callUnit.getLastValid());
//valid已經執行完了,則表示此delay已經檢驗完了--執行目標方法
if (validQueue.size() == 0) {
callUnit.getAction().call();
//把這個任務移出
delaysActions.remove(callUnit);
} else {
Valid valid = callUnit.getValidQueue().peek();
callUnit.setLastValid(valid);
//是否會有後置任務
valid.doValid();
}
}
}
}
複製代碼
最終是調用ActionManager.instance().checkValid()的方法,就是判斷上一個valid是否執行成功,若是沒有成功,則會報出異常。提示必須知足check()爲true後,才能執行下一個valid.若是你永遠都不想目標行爲執行過去,就不要調用CallUnit.reCall()方法便可。若是上一個valid執行成功,則會再調用下一個valid,直到全部的valid都執行完成後,則進入callUnit.getAction().call()的執行。最後進入訂單折扣界面了。
ps:其實工程也實現了註解調用的實現。可是前提是全部的檢驗模型不須要傳入額外的參數才行。 具體看代碼
/**
* 經過反射註解來組裝(可是這個前提是無參的構造方法才行)
*
* @param action
*/
public void postCallUnit(Action action) {
Class clz = action.getClass();
try {
Method method = clz.getMethod("call");
Interceptor interceptor = method.getAnnotation(Interceptor.class);
Class<? extends Valid>[] clzArray = interceptor.value();
CallUnit callUnit = new CallUnit(action);
for (Class cla : clzArray) {
callUnit.addValid((Valid) cla.newInstance());
}
postCallUnit(callUnit);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
複製代碼
只須要進行登陸的驗證
需同時進行登陸和優惠券的驗證
最後放下完整的代碼連接庫,若是對你有幫助,記得star哦