Android对话框的最佳实践 - 如何避免在关闭对话框时应用程序崩溃

3

我在我的应用程序中使用 AlertDialog,在需要进行一些网络操作时,该对话框会向用户显示一些文本,并阻止用户与应用程序交互,直到网络操作完成为止。当网络操作完成后,我将调用 dismiss 来关闭对话框,然后用户就可以再次与应用程序交互了。我的代码大致如下:

//pseudo-code runs in UI/main thread
private void fun() {
    //business code here...
    
    mainExecutor.execute(new Runnable() {
        @Override
        public void run() {
            int timeLeftSec = 10;
            while (true)
            {
                final int timeLeft = timeLeftSec--;
                final String msg = "Time left ";
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        ShowDialog(msg + " " + timeLeft + " s");
                    }
                });
                
                if (CheckIfNetworkOpDone() || timeLeft < 0) {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            DismissProcessDialog();
                        }
                    });
                    break;
                } else {
                    try {
                        Thread.sleep(1000);
                    }catch (Exception e)
                    {
                        //
                    }
                }
            }
        }
    }
}

private void ShowDialog(String info)
{
    if (mainDialog == null || mainDialogTextView == null)
    {
        View dialogView = LayoutInflater.from(this).inflate(R.layout.processbar_dialog, null);

        if (mainDialog == null)
        {
            mainDialog = new AlertDialog.Builder(this).create();
            mainDialog.setView(dialogView);
            mainDialog.setCancelable(false);
            mainDialog.setCanceledOnTouchOutside(false);
        }
        if (mainDialogTextView == null)
        {
            mainDialogTextView = dialogView.findViewById(R.id.dialogTextView);
        }
    }

    mainDialogTextView.setText(info);
    mainDialog.show();
}

private void DismissProcessDialog()
{
    if (mainDialog != null)
    {
        try {
            Activity activity = (Activity) mainActivityContext;

            if (activity == null || activity.isDestroyed() || activity.isFinishing())
            {
                return;
            }

            Context context = ((ContextWrapper)(mainDialog.getContext())).getBaseContext();
            if (!(context instanceof Activity ))
            {
                return;
            }
            activity = (Activity) context;
            if (activity == null || activity.isDestroyed() || activity.isFinishing())
            {
                return;
            }

            if (mainDialog != null && mainDialog.isShowing())
            {
                mainDialog.dismiss();
            }
        }
        catch (Exception e)
        {
            //
        }
    }
}

    @Override
    protected void onDestroy() {
        if (mainDialog != null && mainDialog.isShowing())
        {
            mainDialog.dismiss();
        }
        mainDialog = null;

        super.onDestroy();
    }

但是 Google Play 后端显示有很多关于“dismiss”崩溃的日志。异常信息如下:

java.lang.IllegalArgumentException: 
  at android.view.WindowManagerGlobal.findViewLocked (WindowManagerGlobal.java:517)
  at android.view.WindowManagerGlobal.removeView (WindowManagerGlobal.java:426)
  at android.view.WindowManagerImpl.removeViewImmediate (WindowManagerImpl.java:126)
  at android.app.Dialog.dismissDialog (Dialog.java:389)
  at android.app.-$$Lambda$oslF4K8Uk6v-6nTRoaEpCmfAptE.run (Unknown Source:2)
  at android.os.Handler.handleCallback (Handler.java:883)
  at android.os.Handler.dispatchMessage (Handler.java:100)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7356)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:930)

ProgressDialog已被弃用,我使用AlertDialog替代。我不使用ProgressBar的原因是,我想阻止用户操作,直到网络操作完成。我发现当ProgressBar显示时,用户仍然可以按按钮。

有没有最佳实践来解决这种崩溃或如何处理此场景以正确显示对话框?

任何建议将不胜感激。谢谢!


@Monster Brain,感谢您的建议,但是您可能没有阅读我的代码,因为我已经在代码中检查了活动和对话状态。但它仍然崩溃。 - Thomas
你尝试过使用 dismissAllowingStateLoss() 吗? - Zee
我认为问题可能出现在DismissProcessDialog中,但是从崩溃日志中我无法获取任何与我的代码相关的信息。最近我只是添加了onDestroy方法,在那之前就已经有一些崩溃日志了。所以我猜测问题应该来自于DismissProcessDialog。 - Thomas
@Zee 这不是DialogFragment,但我想试一试。 - Thomas
@Thomas 哎呀!抱歉,是我的错。 - Zee
显示剩余5条评论
5个回答

1

您考虑过如何管理应用程序中的用户交互吗?

有时我会使用我工具类中定义的这两种方法。

public class Utils {
    public static void disableUserInteraction(Activity activity) {
        activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
    }

    public static void enableUserInteraction(Activity activity) {
        activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
    }
}

"我这样称呼它们:"
 Utils.disableUserInteraction(activity);

上述方法设置适当的标志,以便在需要时忽略用户交互。

0
在我看来,有两个问题,你正在从后台线程调用dismiss dialog。您应该始终从UI线程进行UI调用(runOnUi{...}将有所帮助)。其次,您正在使用while (true)运行循环。这是极其不好的做法,您应该在此处传递一个可以在循环内结束的条件。这将无限期地运行,因此当前第一次通过进行网络调用。假设它立即完成,第二次尝试关闭对话框。然后第三次满足相同的条件,现在您在崩溃日志中找到了findViewLocked,因为它正在尝试从两个后台线程访问同一对象。

  1. handler.post 会确保它在 UI 线程上运行,否则它会立即崩溃...
  2. 在 if 条件检查中可能看不到退出循环的条件。
- Thomas

0

在这里使用progressbaralertdialog更合理。如果您想要在加载时防止用户与应用程序交互,可以使屏幕中除进度条之外的所有组件都不可见,或者保持它们可见但更改其焦点或可点击性。


我同意禁用其他组件的可点击性,但由于需要处理许多组件,我没有找到一个好的方法来实现这一点... - Thomas
在你的XML中,放置两个布局,其中一个只有进度条,另一个是主要布局。当数据正在加载时,显示带有进度条的布局,并将主布局的可点击性设置为false。@Thomas - MMG

0

当用户在您的网络操作期间离开(或旋转)您的应用程序并且您的Activity(或Fragment)被销毁(或重新创建)时,您的异常可能会发生。然后就没有对话框可以解除了,异常就会被抛出,参见this answer

虽然上面的答案解决了您的问题,但它犯了一个错误,即在后台任务中保持对Activity的引用。我过去所做的是让我的Activity实现一个回调接口,并将这个回调的WeakReference传递给后台任务。然后后台任务必须检查回调(Activity)是否仍然存在。

上述解决方案仍然存在一个问题,即您必须为每个在旋转(或任何其他配置更改)时创建的新Activity重新启动任务。现代方法是让Activity(或Fragment)观察您的ViewModel中的LiveData。请参见official documentation

同时建议使用DialogFragment,因为它在重新创建时会被恢复。普通的AlertDialog在旋转屏幕(或在应用程序被销毁后切换到应用程序)时会消失。


0
在我的情况下,我在 onCreate 中显示对话框,并在成功获取 Firebase 数据后关闭对话框。但是当我获取数据并运行 dialog.dismiss 命令时,应用程序崩溃了。我尝试了各种方法来修复这个问题,但应用程序仍然崩溃。我通过更改 int 值的逻辑来解决了这个问题。我添加了一个观察者来观察这个值的变化。最初,这个值是 0,然后从 Firebase 获取数据后它会改变。当这个值改变时,dialog.dismiss() 命令就会起作用。这是我的模型类 -

IntOserverModel.class

import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class IntOserverModel extends ViewModel {
private MutableLiveData<Integer> intValue = new MutableLiveData<>();

public MutableLiveData<Integer> getIntValue() {
    return intValue;
   }

public void setIntValue(int newValue) {
    intValue.setValue(newValue);
   }
}

这是我的对话 -
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<ProgressBar
    android:id="@+id/progress_bar"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:max="100"
    android:progressDrawable="@drawable/circular_progress" />

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:fontFamily="@font/average_sans"
    android:paddingHorizontal="10dp"
    android:text="Please wait..."
    android:textAlignment="center"
    android:textColor="@color/white"
    android:textSize="20sp"
    android:textStyle="bold" />

</LinearLayout>

在onResume中,我正在显示和关闭这个对话框。
  Dialog dialog = new Dialog(this);
    dialog.setContentView(R.layout.dialog_processing);
    ProgressBar progressBar = dialog.findViewById(R.id.progress_bar);
    dialog.setCancelable(false);
    dialog.show();

    viewModel = new ViewModelProvider(this).get(IntOserverModel.class);
    viewModel.getIntValue().observe(this, newValue -> dialog.dismiss());

    reference.child(UID).addListenerForSingleValueEvent(new  ValueEventListener() {
        @Override
        public void onDataChange(@NonNull DataSnapshot snapshot) {
            Map map = (Map) snapshot.getValue();
            btnCoin.setText(map.get("Coins").toString());
            viewModel.setIntValue(Integer.parseInt(map.get("Coins").toString()));
       }

        @Override
        public void onCancelled(@NonNull DatabaseError error) {
        }
    });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接