項(xiàng)目中經(jīng)常有遇到一個(gè)典型的需求红柱,就是在用戶在需要進(jìn)入A界面的時(shí)候,需要先判斷用戶是否登錄强岸,如果沒有登錄漩仙,則需要先進(jìn)入登錄界面撒遣,如果登錄成功了喳逛,再直接跳轉(zhuǎn)到A界面瞧捌。
需求定義
所以這里有兩個(gè)需求: 1、自動(dòng)跳轉(zhuǎn)到登錄界面 2润文、登錄成功后再自動(dòng)跳轉(zhuǎn)到目標(biāo)A界面
如果我們直接判斷用戶有沒有登錄,提醒用戶登錄殿怜。也沒有讓用戶登錄成功后再直接跳轉(zhuǎn)到目標(biāo)界面典蝌,這樣的用戶體驗(yàn)恐怕是不能滿足一個(gè)高逼格程序員的要求。那么头谜,我們來思考下骏掀,如何才能更加優(yōu)雅的完成這個(gè)工作呢?
當(dāng)然柱告,在開始之前截驮,我們可以先了解下其他人都是怎么做的,畢竟我們可以站在巨人的肩膀上才能看得更遠(yuǎn)际度。
思考可行的方案
首先我們第一個(gè)想到的解決方式葵袭,就是攔截器。如果我們?cè)谶M(jìn)入A界面的時(shí)候乖菱,可以在操作之前加入一個(gè)攔截器的話坡锡,豈不是可以做到在進(jìn)入A界面前的判斷呢蓬网?
在google之后,找到兩個(gè)方案鹉勒。
A帆锋、 Android攔截器 (可以點(diǎn)擊查看)
此方案通過注解。在進(jìn)入目標(biāo)界面A時(shí)禽额,判斷是否有指定的攔截器锯厢,如果有,則檢驗(yàn)是否滿足攔截器要求脯倒,不滿足哲鸳,則執(zhí)行攔截器的處理,處理完成后盔憨,通過onActivityResult最后觸發(fā)invoke的回調(diào)方法徙菠。
此方案和我們需求略有不同,那么說下此方案存在的缺點(diǎn):
1郁岩、用了繼承的方式,來插入invoke的回調(diào)方法婿奔。由于java的單繼承的特性,如果工程中已經(jīng)有基類的情況问慎,調(diào)整起來比較麻煩萍摊。侵入性太高。
2如叼、此方案中冰木,在沒有登錄的情況下,其實(shí)已經(jīng)進(jìn)入了目標(biāo)A頁面笼恰。相應(yīng)的初始化都已經(jīng)執(zhí)行了踊沸。如果沒有登錄成功,這樣工作其實(shí)是白做了社证。如果目標(biāo)A界面要登錄才能進(jìn)入的話逼龟,此方案不符合要求的。
B追葡、我們直接使用路由框架腺律,參考下阿里的ARouter方案,可以看到宜肉,我們可以在固定路由上面插入攔截器匀钧。這里有一篇文章介紹 阿里ARouter攔截器使用及源碼解析
看了文章后,發(fā)現(xiàn)攔截器實(shí)現(xiàn)的非常優(yōu)雅谬返,但是依然不是我們想要的之斯。因?yàn)檫@個(gè)攔截器執(zhí)行完后,馬上會(huì)執(zhí)行目標(biāo)方法朱浴。中間并不會(huì)等待吊圾。所以我們根本沒有辦法去執(zhí)行我們的登錄操作达椰。 所以pass了。
我們?cè)倩剡^頭來思考项乒,攔截器似乎并不能直接完成我們的需求啰劲,因?yàn)槲覀冃枰迦胍粋€(gè)驗(yàn)證行為后(例如進(jìn)入登錄界面),還要執(zhí)行相應(yīng)的操作后檀何,保證這個(gè)驗(yàn)證行為通過后蝇裤,才能真正進(jìn)入到我們的目標(biāo)界面。
其實(shí)如果我們只是單純的完成這個(gè)功能的話频鉴,可能大家最容易想到的就是栓辜,在進(jìn)入登錄界面的時(shí)候,在intent中裝載一個(gè)目標(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);
這種方式做起來非常直接周荐,也可理解狭莱,但是最明顯的問題就是,會(huì)導(dǎo)致登錄界面多了很多與自己無關(guān)的業(yè)務(wù)判斷概作。那我們繼續(xù)google看看腋妙,有沒有類似的做法,并且實(shí)現(xiàn)優(yōu)雅一點(diǎn)的呢讯榕?
Android 登錄判斷器骤素,登錄成功后幫你準(zhǔn)確跳轉(zhuǎn)到目標(biāo)activity 這篇的訪問量比較大,似乎是個(gè)比較靠譜的方法愚屁。我們來大概分析下它的做法济竹。
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);
}
我們看上面的核心代碼就是,封裝一個(gè)LoginCarrier集绰。如果沒有登錄规辱,則把這個(gè)LoginCarrier傳入到登錄界面。登錄成功后栽燕,觸發(fā)invoke()方法。本質(zhì)上和我們上面的想法差不多改淑。
看完之后碍岔,還是覺得實(shí)現(xiàn)上不夠完美,總覺得有些缺點(diǎn)朵夏。例如
1蔼啦、在登錄界面還是侵入了過多的邏輯(這似乎不可避免,但是否可以簡(jiǎn)潔些呢)
2仰猖、擴(kuò)展性比較差捏肢。比方說我要購買某個(gè)禮品奈籽,需要登錄,然后再跳轉(zhuǎn)到充值界面充值完成后再回來鸵赫。
那到底有沒有更好的實(shí)現(xiàn)方案呢衣屏,谷歌后,發(fā)現(xiàn)暫時(shí)沒有找到可靠的方案了辩棒,所以說靠天靠地狼忱,不如靠自己,既然找不到合適的方案一睁,那就好好思考下钻弄,自己動(dòng)手來干了。
首先者吁,我們?cè)倩剡^頭考慮我們的需求窘俺,我們需要執(zhí)行一個(gè)目標(biāo)方法。但是目標(biāo)方法需要一個(gè)前置的條件滿足才能執(zhí)行复凳,并且這個(gè)前置條件可能不只一個(gè)瘤泪,還有就是這個(gè)前置條件并不是馬上就能完成的。
那我們根據(jù)需求抽象出來的數(shù)據(jù)模型應(yīng)該是染坯。
public class CallUnit {
//目標(biāo)行為
private Action action;
//先進(jìn)先出驗(yàn)證模型
private Queue<Valid> validQueue = new ArrayDeque<>();
//上一個(gè)執(zhí)行的valid
private Valid lastValid;
}
那么目標(biāo)行為action就是一個(gè)執(zhí)行體均芽。負(fù)責(zé)執(zhí)行目標(biāo)方法。
public interface Action {
void call();
}
驗(yàn)證操作validQueue保存一個(gè)驗(yàn)證隊(duì)列单鹿,Valid的驗(yàn)證模型是
public interface Valid {
/**
* 是否滿足檢驗(yàn)器的要求掀宋,如果不滿足的話,則執(zhí)行doValid()方法仲锄。如果滿足劲妙,則執(zhí)行目標(biāo)action.call
* @return
*/
boolean check();
//去執(zhí)行驗(yàn)證前置行為,例如跳轉(zhuǎn)到登錄界面儒喊。(但并未完成驗(yàn)證镣奋。)
void doValid();
}
那么整個(gè)邏輯用一幅圖表達(dá)出來,會(huì)比較清楚怀愧。
接下來根據(jù)圖侨颈,來講解代碼實(shí)現(xiàn)。
第一步芯义,我們需要構(gòu)造一個(gè)CallUnit單元哈垢。例如,我們需要跳轉(zhuǎn)到折扣界面扛拨,前置是我們必須要登錄耘分,并且要有折扣碼。
所以這里,我們有兩個(gè)驗(yàn)證模型求泰,一個(gè)是登錄央渣,一個(gè)是拿到折扣。
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)造一個(gè)執(zhí)行體渴频。直接在當(dāng)前的Activity里面實(shí)現(xiàn)Action接口即可芽丹。例如我們?cè)贛ainActivity中實(shí)現(xiàn)。
@Override
public void call() {
//這是我們的目標(biāo)行為
OrderDetailActivity.startActivity(MainActivity.this, "1234");
}
接下來枉氮,我們就可以構(gòu)造一個(gè)CallUnit對(duì)象并進(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().我們看下這個(gè)單例有啥作用
public class ActionManager {
static ActionManager instance = new ActionManager();
public static ActionManager instance() {
return instance;
}
Stack<CallUnit> delaysActions = new Stack<>();
....
}
這個(gè)單例維護(hù)了一個(gè)CallUnit的堆棧楼肪,表示我們支持一個(gè)目標(biāo)行為里面再嵌入一個(gè)目標(biāo)行為。但是這個(gè)需求恐怕很少會(huì)遇到惹悄。但是設(shè)計(jì)上是支持的春叫。
我們?cè)倩剡^頭看看,postCallUnit()到底做了啥泣港?
/**
* 根據(jù)條件判斷暂殖,是否要執(zhí)行一個(gè)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);
//是否會(huì)有后置任務(wù)
valid.doValid();
}
}
備注非常清楚当纱,就是判斷是否驗(yàn)證條件都滿足呛每,如果滿足,則直接執(zhí)行目標(biāo)方法坡氯,如果不滿足晨横,則執(zhí)行doValid方法。并且保存當(dāng)前valid的引用箫柳,以便后面驗(yàn)證valid是否滿足條件手形。如果不滿足,是不允許再執(zhí)行下一輪的驗(yàn)證悯恍。
到這里库糠,我們知道,我們已經(jīng)觸發(fā)了執(zhí)行體涮毫,并順利進(jìn)入了登錄驗(yàn)證的執(zhí)行體瞬欧。因?yàn)榈卿涍@個(gè)操作需要用戶手動(dòng)觸發(fā)完成,我們只是引導(dǎo)用戶到了登錄界面(當(dāng)然登錄操作也可以代碼自動(dòng)完成罢防,那就沒有必要跳頁面了)黍判,由于我們因?yàn)榈却脩舻妮斎耄覀兊尿?yàn)證模型就在這里停下來了篙梢,如果登錄成功了,我們才需要讓整個(gè)驗(yàn)證模型再運(yùn)轉(zhuǎn)起來了美旧,所以驗(yàn)證后渤滞,永遠(yuǎn)少不了手動(dòng)開啟驗(yàn)證模型贬墩。
例如我們?cè)诘卿洺晒螅枰{(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)檢驗(yàn)完了--執(zhí)行目標(biāo)方法
if (validQueue.size() == 0) {
callUnit.getAction().call();
//把這個(gè)任務(wù)移出
delaysActions.remove(callUnit);
} else {
Valid valid = callUnit.getValidQueue().peek();
callUnit.setLastValid(valid);
//是否會(huì)有后置任務(wù)
valid.doValid();
}
}
}
}
最終是調(diào)用ActionManager.instance().checkValid()的方法,就是判斷上一個(gè)valid是否執(zhí)行成功绪励,如果沒有成功肿孵,則會(huì)報(bào)出異常。提示必須滿足check()為true后疏魏,才能執(zhí)行下一個(gè)valid.如果你永遠(yuǎn)都不想目標(biāo)行為執(zhí)行過去停做,就不要調(diào)用CallUnit.reCall()方法即可。如果上一個(gè)valid執(zhí)行成功大莫,則會(huì)再調(diào)用下一個(gè)valid蛉腌,直到所有的valid都執(zhí)行完成后,則進(jìn)入callUnit.getAction().call()的執(zhí)行只厘。最后進(jìn)入訂單折扣界面了烙丛。
ps:其實(shí)工程也實(shí)現(xiàn)了注解調(diào)用的實(shí)現(xiàn)。但是前提是所有的檢驗(yàn)?zāi)P筒恍枰獋魅腩~外的參數(shù)才行羔味。 具體看代碼
/**
* 通過反射注解來組裝(但是這個(gè)前提是無參的構(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();
}
}
演示流程圖如下
代碼地址
最后放下完整的代碼鏈接庫河咽,如果對(duì)你有幫助,記得star哦