android 解决同一界面多个网络请求导致dialog对话框反复显示消失问题

本文探讨了在多个网络请求并发时,如何优化对话框的显示逻辑,避免对话框频繁出现或过早消失,提出了使用Handler和Runnable的解决方案。


PS :

平时遇到问题到网上搜索博客,作者总是先列出方案1,方案2,方案3, 最后才把最优的解决方式放到最后.

遇到这种情况我总是很焦急, 我心说: “我这得赶紧解决问题,哪有空看你分析啊”, 后来倒是明白了,把自己解决问题的思路写下来,实际上是学习了这种思维方式. 所以我也准备里这么做.

PS: 着急的可以直接看最后的解决方案.

问题:

场景:

当一个界面同一时间有七八个甚至十几个网络请求,这个时候对话框是如何展示的. 我的项目中效果就是对话框只显示一次就消失了. 因为我采取的方案是如果当对话框有并发创建的时候,会先去判断对话框是否显示中,如果显示中就关闭.

具体代码如下:

整体代码

public class LoadingFragment {
    private static WeakReference<Context> contextWeakReference;
    private String content;
    private static Dialog loadingDialog;
    private static AnimationDrawable animationDrawable;
    private static ImageView carImg;

    private LoadingFragment() {
    }

    public static Dialog showLodingDialog(Context context) {
        return showLodingDialog(context, true);
    }

    /**
     * @param context
     * @param mIsCancel true  返回键不取消  false 返回键取消
     * @return
     */

    public static Dialog showLodingDialog(Context context, boolean mIsCancel) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.progress_hud_2, null);// 得到加载view

        carImg = v.findViewById(R.id.spinnerImageView);
        TextView tvContent = v.findViewById(R.id.message);
        Animation rotateAnimation = AnimationUtils.loadAnimation(context, R.anim.rotate_anim);
        LinearInterpolator lin = new LinearInterpolator();
        rotateAnimation.setInterpolator(lin);
        carImg.startAnimation(rotateAnimation);
        tvContent.setText("加载中...");
        if (null != loadingDialog && loadingDialog.isShowing()) {
            loadingDialog.dismiss();
        }
        loadingDialog = new Dialog(context, R.style.ProgressHUD);// 创建自定义样式dialog
        loadingDialog.setCancelable(true); // 是否可以按“返回键”消失
        loadingDialog.setCanceledOnTouchOutside(false); // 点击加载框以外的区域
        loadingDialog.setContentView(v, new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.MATCH_PARENT));// 设置布局


        if (mIsCancel)//点击返回键不取消
            loadingDialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
                public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                    if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
                        return true;
                    } else {
                        return false;
                    }
                }
            });


        //将显示Dialog的方法封装在这里面
        Window window = loadingDialog.getWindow();
        if (window != null) {
            window.clearFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND | WindowManager.LayoutParams.FLAG_DIM_BEHIND);
            WindowManager.LayoutParams lp = window.getAttributes();
            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
            lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
            window.setGravity(Gravity.CENTER);
            window.setAttributes(lp);
        }
        //    window.setWindowAnimations(R.style.PopWindowAnimStyle);
        if (context.getClass().getName().contains("Activity") && !((Activity) context).isFinishing()) {
            loadingDialog.show();
        } else {
            LogUtil.d("展示对话框前先消失");
            dismiss();
        }

        return loadingDialog;
    }


    /**
     * 关闭dialog
     */

    public static void dismiss() {
        if (null != loadingDialog && loadingDialog.isShowing()) {
            loadingDialog.dismiss();
            loadingDialog = null;
        }

        if (null != carImg) {
            carImg.clearAnimation();
        }

    }
}

可以看见关键的代码在这里

  if (null != loadingDialog && loadingDialog.isShowing()) {
            loadingDialog.dismiss();
        }

结果就是对话框只展示了一次.那么最终效果肯定不理想.而且如果网络请求没结束,会有很多异常信息.
导致界面显示数据错乱,甚至会crash.那么如何才能让所有网络请求都结束之后对话框才消失?

方案1: 使用rxjava zip 函数

private void RxZip(VipCardInfoBean vipCardInfoBean, PostListener rxPostListener) {

        String cardNum = vipCardInfoBean.getEcardNo();

        Map<String, String> map = HttpPackageParams.getCardNumParms(cardNum);

        Observable observable1 = Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                //券
                XHttp.getInstance().post(getActivity(), HttpConfig.MEMBER_COUPONS_COUNT, map, new HttpCallBack<String>() {
                    @Override
                    public void onSuccess(String string) {
                        emitter.onNext(string);
                        emitter.onComplete();
                    }

                    @Override
                    public void onFailed(int errorCode, String error) {
                        super.onFailed(errorCode, error);
                        emitter.onNext("-");
                        emitter.onComplete();
                    }

                }, false, true);
            }
        }).subscribeOn(Schedulers.io());

        Observable observable2 = Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                //进行网路请求
                //积分
                XHttp.getInstance().post(getActivity(), HttpConfig.MEMBER_INTEGRAL_COUNT, map, new HttpCallBack<String>() {
                    @Override
                    public void onSuccess(String string) {
                        emitter.onNext(string);
                        emitter.onComplete();
                    }


                    @Override
                    public void onFailed(int errorCode, String error) {
                        super.onFailed(errorCode, error);
                        emitter.onNext("-");
                        emitter.onComplete();
                    }


                }, false, false);


            }
        }).subscribeOn(Schedulers.io());


        Observable observable3 = Observable.create(new ObservableOnSubscribe<String>() {

            @Override
            public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                //储值
                XHttp.getInstance().post(getActivity(), HttpConfig.FIND_MEMBER_SAVING_COUNT, map, new HttpCallBack<String>() {
                    @Override
                    public void onSuccess(String string) {
                        ElectronicCardBean electronicCardBean = GsonUtil.GsonToBean(string, ElectronicCardBean.class);
                        if (electronicCardBean != null && !TextUtils.isEmpty(electronicCardBean.getEcardBalance())) {
//                            updateAdapter(position, "", "", electronicCardBean.getEcardBalance(), true);
                            emitter.onNext(electronicCardBean.getEcardBalance());
                            emitter.onComplete();
                        } else {
                            emitter.onNext("-");
                            emitter.onComplete();
                        }
                    }


                    @Override
                    public void onFailed(int errorCode, String error) {
                        super.onFailed(errorCode, error);
//                        updateAdapter(position, "", "", "", false);
                        emitter.onNext("-");
                        emitter.onComplete();
                    }

                }, false, false);
            }
        }).subscribeOn(Schedulers.io());

        Observable.zip(observable1, observable2, observable3, new Function3<String, String, String, CardMoneyBean>() {

            @Override
            public CardMoneyBean apply(String string1, String string2, String string3) throws Exception {
                LogUtil.d("优惠券 = " + string1);
                LogUtil.d("积分 = " + string2);
                LogUtil.d("电子余额 = " + string3);
                CardMoneyBean cardMoneyBean = new CardMoneyBean();
                cardMoneyBean.setCoupon(string1);

                BigDecimal bg = new BigDecimal(string2).setScale(2, BigDecimal.ROUND_HALF_UP);
                // 不足两位小数补0
                DecimalFormat decimalFormat = new DecimalFormat("0.00#");
                String strVal = decimalFormat.format(bg);
                vipCardInfoBean.setPoint(strVal);
                cardMoneyBean.setPoint(strVal);
                cardMoneyBean.setMoney(string3);
                return cardMoneyBean;
            }
        }).subscribe(new Consumer<CardMoneyBean>() {
            @Override
            public void accept(CardMoneyBean cardMoneyBean1) throws Exception {

                LogUtil.d("cardMoneyBean1 = " + GsonUtil.gsonString(cardMoneyBean1));
                if ("-".equals(cardMoneyBean1.getMoney())) {
                    cardMoneyBean1.setHasEcard(false);
                } else {
                    cardMoneyBean1.setHasEcard(true);
                }

                vipCardInfoBean.setCardMoneyBean(cardMoneyBean1);

                postCount++;
                if (postCount == list.size()) {
                    if (rxPostListener != null)
                        rxPostListener.onFinish();
                }
            }

            @Override
            protected void finalize() throws Throwable {
                LogUtil.d("finalize");
                LoadingFragment.dismiss();
                super.finalize();
            }
        });

    }

在这里我只让第一个请求弹出对话框 . 这样就可以形成三个网络请求只显示一个对话框

方案2: 使用java future 类

方案3: 使用计数器

方案3 优化 单例(有异常情况)

思想: 使用一个变量用户存储当前展示的对话框,当有一个网络请求进入的时候对这个变量进行加一(如果变量大于0 就不创建对话框), 在关闭对话框的时候进行判断 如果这个变量大于0 那么说明整个网络请求未结束.不关闭这个对话框,并且对这个变量减一. 如果变量小于=0 则把对话框关闭. ----- (存在多线程并发问题)

2021.2.1 之前曾经尝试使用volatile并不管用,

顺带说下volatile关键字很重要的两个特性:
1、保证变量在线程间可见,对volatile变量所有的写操作都能立即反应到其他线程中,换句话说,volatile变量在各个线程中是一致的(得益于java内存模型—“先行发生原则”);

2、禁止指令的重排序优化;

虽然多线程间可见,但是这个论据并不能得出"基于volatile变量的运算在并发下是安全的"这个结论,因为核心点在于java里的运算(比如自增)并不是原子性的。

发现一个类AtomicInteger类 这个类是原子性的 可以尝试用下这个

方案4: 使用arraylist保存创建的Dialog


 
    public static void showLoading(Context context,String type){
        final AppCompatDialog dialog = new AppCompatDialog(context, R.style.dialog);
        final AVLoadingIndicatorView avLoadingIndicatorView = LoaderCreator.create(type,context);
        dialog.setContentView(avLoadingIndicatorView);
        int deviceWidth = DimenUtil.getScreenWidth(context);
        int deviceHeight = DimenUtil.getScreenHeight(context);
        final Window dialogWindow = dialog.getWindow();
        if (dialogWindow!=null){
            WindowManager.LayoutParams lp = dialogWindow.getAttributes();
            lp.width = deviceWidth/LOADER_SIZE_SCALE;
            lp.height = deviceHeight/LOADER_SIZE_SCALE;
            lp.height = lp.height + deviceHeight/LOADER_OFFSET_SCALE;
            lp.gravity = Gravity.CENTER;
        }
        LOADERS.add(dialog);
        dialog.show();
    }
 
    public static void showLoading(Context context){
        showLoading(context,DEFAULT_LOADER);
    }
 
    public static void stopLoading(){
        for(AppCompatDialog dialog:LOADERS){
            if (dialog!=null){
                if (dialog.isShowing()) {
                    dialog.cancel();
                }
            }
        }
    }
 
    public static void showLoading(Context context,Enum<LoaderStyle> type){
        showLoading(context,type.name());
    }

存在线程并发问题

https://www.jianshu.com/p/fc2e551b907c

最终方案

使用计数器

AtomicInteger

下面的方案存在问题

串行请求的时候会有问题,如果第一个请求结束后再请求第二个就存在问题了.如果请求时间过长,会让对话框提前结束

思考

在计算机的各个领域中,有这样一种设计思想:当一个临时的对象可能会被多次复用时,给它添加一个定时器。某次任务执行完毕后,不要立即回收这个对象,而是开启定时器。在定时器结束前到来的请求,依旧交给它处理,并重置定时。如果超时,则销毁这个对象,下次使用时需要重新创建。如HTTP连接,数据库连接,线程池等等,都会用到这种思想。实际上我们的弹窗应用场景与这些也是十分类似,我们可以用相同的方法来解决这个问题。

在某次网络请求结束之后,不要立即取消弹窗,而是开启一个定时任务去取消。如果在定时结束之前再次发起请求,则继续使用这个弹窗,并重置定时。这样我们就算是优化了这个逻辑.

伪代码实现

  1. 把取消弹窗的逻辑放在Runnable 对象中,当调用dismissProgressDialog时,不是直接执行runnable,而是通过Handler.postDelay()方法 ,在200ms(这个时间可以自由控制 )后再取消弹窗。
  2. 而再进行弹窗的时候,先判断该runnable是否在消息队列中。如果还在,说明上次弹窗尚未被取消。因此只需要从消息队列中移除runnable即可保持弹窗还在。如果不存在,就重新弹窗

代码实现


package com.sinochem.www.car.owner.view;

import android.androidlib.utils.GlideUtils;
import android.androidlib.utils.Utils;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.sinochem.www.car.owner.R;
import com.sinochem.www.car.owner.utils.LogUtil;

import java.lang.ref.WeakReference;

import androidx.appcompat.app.AppCompatDialog;


public class LoadingFragment {
    private static WeakReference<Context> contextWeakReference;
    private String content;
    private static AppCompatDialog loadingDialog;

    private LoadingFragment() {
    }

    static Handler mHandler = new Handler();

    private static Runnable runnable = new Runnable() {
        @Override
        public void run() {
            LogUtil.d("runnable调用关闭对话框");
            Context mContext = contextWeakReference.get();
            if (mContext!=null && mContext.getClass().getName().contains("Activity") && !Utils.isDestroy((Activity) mContext)){
                if (loadingDialog != null && loadingDialog.isShowing() == true) {
                    loadingDialog.dismiss();
                }
            }
            loadingDialog = null;
        }
    };

    public static void showLodingDialog(Context context) {

        contextWeakReference = new WeakReference<>(context);
        Context mContext = contextWeakReference.get();
        showLodingDialogGif(mContext, false);
    }

    /**
     * @param context
     * @param mIsCancel true  返回键不取消  false 返回键取消
     * @return
     */


    private static Dialog showLodingDialogGif(Context context, boolean mIsCancel) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.loading_progress, null);// 得到加载view

        ImageView gif = v.findViewById(R.id.iv_gif);
        GlideUtils.loadFifLoop(context, R.mipmap.loading_gif, gif);

        TextView tvContent = v.findViewById(R.id.message);
        tvContent.setText("加载中...");
        // 创建自定义样式dialog

        if (loadingDialog == null) {

            loadingDialog = new AppCompatDialog(context, R.style.ProgressHUD);
            loadingDialog.setCancelable(true); // 是否可以按“返回键”消失
            loadingDialog.setCanceledOnTouchOutside(false); // 点击加载框以外的区域
            loadingDialog.setContentView(v, new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.MATCH_PARENT));// 设置布局
            if (mIsCancel)//点击返回键不取消
                loadingDialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
                    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
                            return true;
                        } else {
                            return false;
                        }
                    }
                });


            //将显示Dialog的方法封装在这里面
            Window window = loadingDialog.getWindow();
            if (window != null) {
                window.clearFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND | WindowManager.LayoutParams.FLAG_DIM_BEHIND);
                WindowManager.LayoutParams lp = window.getAttributes();
                lp.width = WindowManager.LayoutParams.MATCH_PARENT;
                lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
                window.setGravity(Gravity.CENTER);
                window.setAttributes(lp);
            }
            //    window.setWindowAnimations(R.style.PopWindowAnimStyle);
            if (context.getClass().getName().contains("Activity") && !Utils.isDestroy((Activity) context)) {
                loadingDialog.show();
//            LOADERS.add(loadingDialog);
//            LogUtil.d("对话框show 数量= " + LOADERS.size());
            } else {
                LogUtil.d("展示对话框前先消失");
                dismiss();
            }
        } else {
            // 上一个dialog还存在,先不取消,继续使用该dialog
            mHandler.removeCallbacks(runnable);
        } 


        return loadingDialog;
    }


    private static Dialog showLodingDialog(Context context, boolean mIsCancel) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.progress_hud_2, null);// 得到加载view

//        carImg = v.findViewById(R.id.spinnerImageView);
        TextView tvContent = v.findViewById(R.id.message);
//        Animation rotateAnimation = AnimationUtils.loadAnimation(context, R.anim.rotate_anim);
//        LinearInterpolator lin = new LinearInterpolator();
//        rotateAnimation.setInterpolator(lin);
//        carImg.startAnimation(rotateAnimation);
        tvContent.setText("加载中...");

        AppCompatDialog loadingDialog = new AppCompatDialog(context, R.style.ProgressHUD);// 创建自定义样式dialog
        loadingDialog.setCancelable(true); // 是否可以按“返回键”消失
        loadingDialog.setCanceledOnTouchOutside(false); // 点击加载框以外的区域
        loadingDialog.setContentView(v, new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.MATCH_PARENT));// 设置布局

        if (mIsCancel)//点击返回键不取消
            loadingDialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
                public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                    if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
                        return true;
                    } else {
                        return false;
                    }
                }
            });


        //将显示Dialog的方法封装在这里面
        Window window = loadingDialog.getWindow();
        if (window != null) {
            window.clearFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND | WindowManager.LayoutParams.FLAG_DIM_BEHIND);
            WindowManager.LayoutParams lp = window.getAttributes();
            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
            lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
            window.setGravity(Gravity.CENTER);
            window.setAttributes(lp);
        }
        //    window.setWindowAnimations(R.style.PopWindowAnimStyle);
        if (context.getClass().getName().contains("Activity") && !((Activity) context).isFinishing()) {
//            LOADERS.add(loadingDialog);
            loadingDialog.show();
//            LogUtil.d("对话框show 数量= " + LOADERS.size());
        } else {
            LogUtil.d("展示对话框前先消失");
            dismiss();
        }

        return loadingDialog;
    }



    /**
     * 关闭dialog
     */


    public static void dismiss() {

        if (null != loadingDialog && loadingDialog.isShowing()) {
            //// 延迟200ms再结束弹窗,简化连续调用接口的进度条管理
            LogUtil.d("延迟200ms再结束弹窗");
            mHandler.postDelayed(runnable, 200);
            return;
        }

        // 1.移除Handler中可能存在的runnable 2.立即结束弹窗
        // 通常用在Activity Destroy中,防止窗体泄漏
        mHandler.removeCallbacks(runnable);
        LogUtil.d("移除Handler中可能存在的runnable");
        if (null != loadingDialog && loadingDialog.isShowing()) {
            LogUtil.d("直接关闭对话框");
            loadingDialog.dismiss();
            loadingDialog = null;
        }
    }
}




关键代码

关闭对话框

   if (null != loadingDialog && loadingDialog.isShowing()) {
            //// 延迟200ms再结束弹窗,简化连续调用接口的进度条管理
            LogUtil.d("延迟200ms再结束弹窗");
            mHandler.postDelayed(runnable, 200);
            return;
        }
        mHandler.removeCallbacks(runnable);
  static Handler mHandler = new Handler();

    private static Runnable runnable = new Runnable() {
        @Override
        public void run() {
            LogUtil.d("runnable调用关闭对话框");
            Context mContext = contextWeakReference.get();
            if (mContext!=null && mContext.getClass().getName().contains("Activity") && !Utils.isDestroy((Activity) mContext)){
                if (loadingDialog != null && loadingDialog.isShowing() == true) {
                    loadingDialog.dismiss();
                }
            }
            loadingDialog = null;
        }
    };

创建时候判断是否有对话框,如果有对话框吧之前的消息队列删除


        if (loadingDialog == null) {
        }else{
            mHandler.removeCallbacks(runnable);
        }

串行请求的情况

如果有串行请求的情况 那么可以单独的吧对话框提到前面来 比如有两个请求, 那么在第一个请求之前弹出对话框,成功后不关闭对话框,直接进行下一个网络请求 等待第二个网络请求结束后 才关闭对话框

或者使用rxjava 解决串行问题

补充

上面代码还是有点问题, 主要出现在多线程并发情况, 对dialog的判断,是多线程并发可见性的问题, 需要把判断条件做一下改变,定义一个boolean 类型的变量isShow,对他使用volatile进行修饰,以及用这个值进行对话框是否展示的判定, 因为对基本类型赋值是原子性的. 用volatile 修饰让各线程都可见.这样就真正解决多线程并发的问题了

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值