android 關於先登入成功後再進入目標介面的思考

煉石發表於2017-12-11

原本只是想把自己的思路和想法給大家分享一下,沒想到有這麼多人的關注和喜歡,實在是有點受寵苦驚。或許是思路太長,給某些人造成了誤解,在此做一個說明。(如果還沒有看過文章,可以直接從下面分隔線開始)

  1. 這裡講的跟網路攔截沒有什麼關係。

  2. 這裡講的不僅僅是登入跳轉,而是由登入跳轉引出的一個特殊場景需求。就是有前置條件下延遲任務處理的問題。

  3. 因為本人沒有找到更好的方案,所以做了這個設計。希望有更好方案的朋友留言提出。

另外收到部分朋友的反饋,任務邏輯處理的不夠簡潔。特此修改了第二版的實現。核心程式碼如下

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();

}
}複製程式碼

另外筆者本人也根據自己平時的業務需求,總結了如下幾種應用場景。

延遲任務場景.png

這裡總結下,這樣做的好處。

1、完全支援上面所有的情形。不用做特殊判斷。

2、圖中黃色區域,都在主介面所在的上下文中執行。邏輯就在當前介面,不會到無關介面中處理。做到了職責清晰。

3、呼叫起來更加簡單。

如果介紹不清楚,請直接看程式碼。真的是比較簡單的。

—————————-這是分隔線————————–

專案中經常有遇到一個典型的需求,就是在使用者在需要進入A介面的時候,需要先判斷使用者是否登入,如果沒有登入,則需要先進入登入介面,如果登入成功了,再直接跳轉到A介面。

需求定義

所以這裡有兩個需求: 1、自動跳轉到登入介面 2、登入成功後再自動跳轉到目標A介面

如果我們直接判斷使用者有沒有登入,提醒使用者登入。也沒有讓使用者登入成功後再直接跳轉到目標介面,這樣的使用者體驗恐怕是不能滿足一個高逼格程式設計師的要求。那麼,我們來思考下,如何才能更加優雅的完成這個工作呢?

當然,在開始之前,我們可以先了解下其他人都是怎麼做的,畢竟我們可以站在巨人的肩膀上才能看得更遠。

思考可行的方案

首先我們第一個想到的解決方式,就是攔截器。如果我們在進入A介面的時候,可以在操作之前加入一個攔截器的話,豈不是可以做到在進入A介面前的判斷呢?

在google之後,找到兩個方案。

A、 Android攔截器 (可以點選檢視)

此方案通過註解。在進入目標介面A時,判斷是否有指定的攔截器,如果有,則檢驗是否滿足攔截器要求,不滿足,則執行攔截器的處理,處理完成後,通過onActivityResult最後觸發invoke的回撥方法。

此方案和我們需求略有不同,那麼說下此方案存在的缺點:1、用了繼承的方式,來插入invoke的回撥方法。由於java的單繼承的特性,如果工程中已經有基類的情況,調整起來比較麻煩。侵入性太高。

2、此方案中,在沒有登入的情況下,其實已經進入了目標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()方法。本質上和我們上面的想法差不多。

看完之後,還是覺得實現上不夠完美,總覺得有些缺點。例如

1、在登入介面還是侵入了過多的邏輯(這似乎不可避免,但是否可以簡潔些呢)

2、擴充套件性比較差。比方說我要購買某個禮品,需要登入,然後再跳轉到充值介面充值完成後再回來。

那到底有沒有更好的實現方案呢,谷歌後,發現暫時沒有找到可靠的方案了,所以說靠天靠地,不如靠自己,既然找不到合適的方案,那就好好思考下,自己動手來幹了。

首先,我們再回過頭考慮我們的需求,我們需要執行一個目標方法。但是目標方法需要一個前置的條件滿足才能執行,並且這個前置條件可能不只一個,還有就是這個前置條件並不是馬上就能完成的。

那我們根據需求抽象出來的資料模型應該是。

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哦

來源:https://juejin.im/post/5a2d23ed51882531ea653578

相關文章