項目中經(jīng)常有遇到一個典型的需求,就是在用戶在需要進(jìn)入A界面的時候,需要先判斷用戶是否登錄,如果沒有登錄,則需要先進(jìn)入登錄界面,如果登錄成功了,再直接跳轉(zhuǎn)到A界面。
需求定義
所以這里有兩個需求: 1、自動跳轉(zhuǎn)到登錄界面 2、登錄成功后再自動跳轉(zhuǎn)到目標(biāo)A界面
如果我們直接判斷用戶有沒有登錄,提醒用戶登錄。也沒有讓用戶登錄成功后再直接跳轉(zhuǎn)到目標(biāo)界面,這樣的用戶體驗恐怕是不能滿足一個高逼格程序員的要求。那么,我們來思考下,如何才能更加優(yōu)雅的完成這個工作呢?
當(dāng)然,在開始之前,我們可以先了解下其他人都是怎么做的,畢竟我們可以站在巨人的肩膀上才能看得更遠(yuǎn)。
思考可行的方案
首先我們第一個想到的解決方式,就是攔截器。如果我們在進(jìn)入A界面的時候,可以在操作之前加入一個攔截器的話,豈不是可以做到在進(jìn)入A界面前的判斷呢?
在google之后,找到兩個方案。
A、 Android攔截器 (可以點(diǎn)擊查看)
此方案通過注解。在進(jìn)入目標(biāo)界面A時,判斷是否有指定的攔截器,如果有,則檢驗是否滿足攔截器要求,不滿足,則執(zhí)行攔截器的處理,處理完成后,通過onActivityResult最后觸發(fā)invoke的回調(diào)方法。
此方案和我們需求略有不同,那么說下此方案存在的缺點(diǎn):
1、用了繼承的方式,來插入invoke的回調(diào)方法。由于java的單繼承的特性,如果工程中已經(jīng)有基類的情況,調(diào)整起來比較麻煩。侵入性太高。
2、此方案中,在沒有登錄的情況下,其實已經(jīng)進(jìn)入了目標(biāo)A頁面。相應(yīng)的初始化都已經(jīng)執(zhí)行了。如果沒有登錄成功,這樣工作其實是白做了。如果目標(biāo)A界面要登錄才能進(jìn)入的話,此方案不符合要求的。
B、我們直接使用路由框架,參考下阿里的ARouter方案,可以看到,我們可以在固定路由上面插入攔截器。這里有一篇文章介紹 阿里ARouter攔截器使用及源碼解析
看了文章后,發(fā)現(xiàn)攔截器實現(xiàn)的非常優(yōu)雅,但是依然不是我們想要的。因為這個攔截器執(zhí)行完后,馬上會執(zhí)行目標(biāo)方法。中間并不會等待。所以我們根本沒有辦法去執(zhí)行我們的登錄操作。 所以pass了。
我們再回過頭來思考,攔截器似乎并不能直接完成我們的需求,因為我們需要插入一個驗證行為后(例如進(jìn)入登錄界面),還要執(zhí)行相應(yīng)的操作后,保證這個驗證行為通過后,才能真正進(jìn)入到我們的目標(biāo)界面。
其實如果我們只是單純的完成這個功能的話,可能大家最容易想到的就是,在進(jìn)入登錄界面的時候,在intent中裝載一個目標(biāo)target的intent.如果登錄成功了,就判斷是否有目標(biāo)target,如果有,就跳轉(zhuǎn)到目標(biāo)target.
Intent intent = new Intent(this,LoginActivity.class);
Intent target = new Intent(this,OrderDetailActivity.class);
intent.putExtra("target",target);
startActivity(intent);
這種方式做起來非常直接,也可理解,但是最明顯的問題就是,會導(dǎo)致登錄界面多了很多與自己無關(guān)的業(yè)務(wù)判斷。那我們繼續(xù)google看看,有沒有類似的做法,并且實現(xiàn)優(yōu)雅一點(diǎn)的呢?
Android 登錄判斷器,登錄成功后幫你準(zhǔn)確跳轉(zhuǎn)到目標(biāo)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可以跳轉(zhuǎn)", 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傳入到登錄界面。登錄成功后,觸發(fā)invoke()方法。本質(zhì)上和我們上面的想法差不多。
看完之后,還是覺得實現(xiàn)上不夠完美,總覺得有些缺點(diǎn)。例如
1、在登錄界面還是侵入了過多的邏輯(這似乎不可避免,但是否可以簡潔些呢)
2、擴(kuò)展性比較差。比方說我要購買某個禮品,需要登錄,然后再跳轉(zhuǎn)到充值界面充值完成后再回來。
那到底有沒有更好的實現(xiàn)方案呢,谷歌后,發(fā)現(xiàn)暫時沒有找到可靠的方案了,所以說靠天靠地,不如靠自己,既然找不到合適的方案,那就好好思考下,自己動手來干了。
首先,我們再回過頭考慮我們的需求,我們需要執(zhí)行一個目標(biāo)方法。但是目標(biāo)方法需要一個前置的條件滿足才能執(zhí)行,并且這個前置條件可能不只一個,還有就是這個前置條件并不是馬上就能完成的。
那我們根據(jù)需求抽象出來的數(shù)據(jù)模型應(yīng)該是。
public class CallUnit {
//目標(biāo)行為
private Action action;
//先進(jìn)先出驗證模型
private Queue<Valid> validQueue = new ArrayDeque<>();
//上一個執(zhí)行的valid
private Valid lastValid;
}
那么目標(biāo)行為action就是一個執(zhí)行體。負(fù)責(zé)執(zhí)行目標(biāo)方法。
public interface Action {
void call();
}
驗證操作validQueue保存一個驗證隊列,Valid的驗證模型是
public interface Valid {
/**
* 是否滿足檢驗器的要求,如果不滿足的話,則執(zhí)行doValid()方法。如果滿足,則執(zhí)行目標(biāo)action.call
* @return
*/
boolean check();
//去執(zhí)行驗證前置行為,例如跳轉(zhuǎn)到登錄界面。(但并未完成驗證。)
void doValid();
}
那么整個邏輯用一幅圖表達(dá)出來,會比較清楚。
接下來根據(jù)圖,來講解代碼實現(xiàn)。
第一步,我們需要構(gòu)造一個CallUnit單元。例如,我們需要跳轉(zhuǎn)到折扣界面,前置是我們必須要登錄,并且要有折扣碼。
所以這里,我們有兩個驗證模型,一個是登錄,一個是拿到折扣。
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);
}
}
然后我們需要構(gòu)造一個執(zhí)行體。直接在當(dāng)前的Activity里面實現(xiàn)Action接口即可。例如我們在MainActivity中實現(xiàn)。
@Override
public void call() {
//這是我們的目標(biāo)行為
OrderDetailActivity.startActivity(MainActivity.this, "1234");
}
接下來,我們就可以構(gòu)造一個CallUnit對象并進(jìn)行執(zhí)行了。
CallUnit.newInstance(MainActivity.this)
.addValid(new LoginValid(MainActivity.this))
.addValid(new DiscountValid(MainActivity.this))
.doCall();
我們來看看doCall到底做了什么?
public void doCall(){
ActionManager.instance().postCallUnit(this);
}
發(fā)現(xiàn),我們是通過ActionManager的單例調(diào)用了postCallUnit().我們看下這個單例有啥作用
public class ActionManager {
static ActionManager instance = new ActionManager();
public static ActionManager instance() {
return instance;
}
Stack<CallUnit> delaysActions = new Stack<>();
....
}
這個單例維護(hù)了一個CallUnit的堆棧,表示我們支持一個目標(biāo)行為里面再嵌入一個目標(biāo)行為。但是這個需求恐怕很少會遇到。但是設(shè)計上是支持的。
我們再回過頭看看,postCallUnit()到底做了啥?
/**
* 根據(jù)條件判斷,是否要執(zhí)行一個action
*
* @param callUnit
*/
public void postCallUnit(CallUnit callUnit) {
//清除所有的actions
delaysActions.clear();
//執(zhí)行check
callUnit.check();
//如果全部滿足,則直接跳轉(zhuǎn)目標(biāo)方法
if (callUnit.getValidQueue().size() == 0) {
callUnit.getAction().call();
} else {
//加入到延遲執(zhí)行體中來
delaysActions.push(callUnit);
Valid valid = callUnit.getValidQueue().peek();
callUnit.setLastValid(valid);
//是否會有后置任務(wù)
valid.doValid();
}
}
備注非常清楚,就是判斷是否驗證條件都滿足,如果滿足,則直接執(zhí)行目標(biāo)方法,如果不滿足,則執(zhí)行doValid方法。并且保存當(dāng)前valid的引用,以便后面驗證valid是否滿足條件。如果不滿足,是不允許再執(zhí)行下一輪的驗證。
到這里,我們知道,我們已經(jīng)觸發(fā)了執(zhí)行體,并順利進(jìn)入了登錄驗證的執(zhí)行體。因為登錄這個操作需要用戶手動觸發(fā)完成,我們只是引導(dǎo)用戶到了登錄界面(當(dāng)然登錄操作也可以代碼自動完成,那就沒有必要跳頁面了),由于我們因為等待用戶的輸入,我們的驗證模型就在這里停下來了,如果登錄成功了,我們才需要讓整個驗證模型再運(yùn)轉(zhuǎn)起來了,所以驗證后,永遠(yuǎn)少不了手動開啟驗證模型。
例如我們在登錄成功后,需要調(diào)用方法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);
//這里執(zhí)行延遲的action方法。
CallUnit.reCall();
finish();
}
});
我們看看CallUnit.reCall()的執(zhí)行方法
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已經(jīng)執(zhí)行完了,則表示此delay已經(jīng)檢驗完了--執(zhí)行目標(biāo)方法
if (validQueue.size() == 0) {
callUnit.getAction().call();
//把這個任務(wù)移出
delaysActions.remove(callUnit);
} else {
Valid valid = callUnit.getValidQueue().peek();
callUnit.setLastValid(valid);
//是否會有后置任務(wù)
valid.doValid();
}
}
}
}
最終是調(diào)用ActionManager.instance().checkValid()的方法,就是判斷上一個valid是否執(zhí)行成功,如果沒有成功,則會報出異常。提示必須滿足check()為true后,才能執(zhí)行下一個valid.如果你永遠(yuǎn)都不想目標(biāo)行為執(zhí)行過去,就不要調(diào)用CallUnit.reCall()方法即可。如果上一個valid執(zhí)行成功,則會再調(diào)用下一個valid,直到所有的valid都執(zhí)行完成后,則進(jìn)入callUnit.getAction().call()的執(zhí)行。最后進(jìn)入訂單折扣界面了。
ps:其實工程也實現(xiàn)了注解調(diào)用的實現(xiàn)。但是前提是所有的檢驗?zāi)P筒恍枰獋魅腩~外的參數(shù)才行。 具體看代碼
/**
* 通過反射注解來組裝(但是這個前提是無參的構(gòu)造方法才行)
*
* @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哦