android 关于先登陆成功后再进入目标界面的思考

本来只是想把本身的思路和想法给你们分享一下,没想到有这么多人的关注和喜欢,实在是有点受宠苦惊。或许是思路太长,给某些人形成了误解,在此作一个说明。(若是尚未看过文章,能够直接从下面分隔线开始)java

  1. 这里讲的跟网络拦截没有什么关系。git

  2. 这里讲的不只仅是登陆跳转,而是由登陆跳转引出的一个特殊场景需求。就是有前置条件下延迟任务处理的问题。程序员

  3. 由于本人没有找到更好的方案,因此作了这个设计。但愿有更好方案的朋友留言提出。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();
    }
}

复制代码

另外笔者本人也根据本身平时的业务需求,总结了以下几种应用场景。框架

延迟任务场景.png

这里总结下,这样作的好处。ide

一、彻底支持上面全部的情形。不用作特殊判断。post

二、图中黄色区域,都在主界面所在的上下文中执行。逻辑就在当前界面,不会到无关界面中处理。作到了职责清晰。ui

三、调用起来更加简单。

若是介绍不清楚,请直接看代码。真的是比较简单的。

----------------------------这是分隔线--------------------------

项目中常常有遇到一个典型的需求,就是在用户在须要进入A界面的时候,须要先判断用户是否登陆,若是没有登陆,则须要先进入登陆界面,若是登陆成功了,再直接跳转到A界面。

需求定义

因此这里有两个需求: 一、自动跳转到登陆界面 二、登陆成功后再自动跳转到目标A界面

若是咱们直接判断用户有没有登陆,提醒用户登陆。也没有让用户登陆成功后再直接跳转到目标界面,这样的用户体验恐怕是不能知足一个高逼格程序员的要求。那么,咱们来思考下,如何才能更加优雅的完成这个工做呢?

固然,在开始以前,咱们能够先了解下其余人都是怎么作的,毕竟咱们能够站在巨人的肩膀上才能看得更远。

思考可行的方案

首先咱们第一个想到的解决方式,就是拦截器。若是咱们在进入A界面的时候,能够在操做以前加入一个拦截器的话,岂不是能够作到在进入A界面前的判断呢?

在google以后,找到两个方案。

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哦

相关文章
相关标签/搜索