DialogFragment 不适合复用

最近想要复用 DialogFragment 时,遇到一些坑。看起来这个类并不适合复用。

缘起

导入 DialogFragment:

kts 复制代码
implementation("androidx.fragment:fragment:1.8.6")

创建一个简单的 DialogFragment:

kotlin 复制代码
class MyDialogFragment : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        builder.setTitle("My Dialog")
            .setMessage("This is a dialog fragment.")
            .setPositiveButton("OK", DialogInterface.OnClickListener { dialog: DialogInterface?, which: Int ->
                Toast.makeText(activity, "You clicked OK", Toast.LENGTH_SHORT).show()
            })
            .setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog: DialogInterface?, which: Int ->
                Toast.makeText(activity, "You clicked Cancel", Toast.LENGTH_SHORT).show()
            })
        return builder.create()
    }
}

在 MainActivity 中加个按钮将其 show 出来:

kotlin 复制代码
class MainActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyComposeDialogApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Button(
                        onClick = {
                            onClick()
                        },
                        modifier = Modifier
                            .padding(innerPadding)
                            .fillMaxSize()
                            .wrapContentSize(Alignment.Center)
                    ) {
                        Text(text = "Show Dialog")
                    }
                }
            }
        }
    }


    private fun onClick() {
        showDialog()
    }
    
    val dialog = MyDialogFragment()

    private fun showDialog() {
        dialog.show(supportFragmentManager, "dialog")
    }
}

运行效果:

连续 show

如果调用两次 show 方法:

kotlin 复制代码
private fun onClick() {
    showDialog()
    showDialog()
}

恭喜你遇到这个 Fragment already added crash:

scss 复制代码
FATAL EXCEPTION: main (Ask Gemini)
Process: com.kevintest.mycomposedialogapplication, PID: 30587
java.lang.IllegalStateException: Fragment already added: MyDialogFragment{7a3a5ec} (08f89801-c16f-4c68-b7a9-b1146a5f9958 tag=dialog)
	at androidx.fragment.app.FragmentStore.addFragment(FragmentStore.java:93)
	at androidx.fragment.app.FragmentManager.addFragment(FragmentManager.java:1733)
	at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:389)
	at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2280)
	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2165)
	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2115)
	at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2052)
	at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:703)
	at android.os.Handler.handleCallback(Handler.java:959)
	at android.os.Handler.dispatchMessage(Handler.java:100)
	at android.os.Looper.loopOnce(Looper.java:232)
	at android.os.Looper.loop(Looper.java:317)
	at android.app.ActivityThread.main(ActivityThread.java:8705)
  at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)

查看 show 方法源码:

less 复制代码
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.setReorderingAllowed(true);
    ft.add(this, tag);
    ft.commit();
}

这个错误看起来很清晰,由于 DialogFragment 已经被 add 过一次了,再次 show 时又会再次 add。所以不能这样连续调用 show。

笔者在实际工作中遇到的问题是:当收到网络请求出错时,显示此 dialog。如果连续有多个错误,那么就会出现多次调用 show 方法。于是偶尔就会出现 crash。

判断 isAdded

那么在 show 之前检查 isAdded 不就行了,天真的我这样想着:

kotlin 复制代码
private fun onClick() {
    showDialog()
    showDialog()
}

val dialog = MyDialogFragment()

private fun showDialog() {
    if (dialog.isAdded) {
        Log.d("MainActivity", "Dialog is already added")
        return
    }
    dialog.show(supportFragmentManager, "dialog")
}

再次运行,仍然报一样的错。

这是因为 show 方法是异步的,调用 show 之后,isAdded 并不是马上变成 true。而是要等一段时间才会变成 true。也就是说这样就没有问题:

kotlin 复制代码
val dialog = MyDialogFragment()

private fun onClick() {
    dialog.show(supportFragmentManager, "dialog")
    Handler(Looper.getMainLooper()).postDelayed({
        if (!dialog.isAdded) {
            dialog.show(supportFragmentManager, "dialog")
        } else {
            Log.d("MainActivity", "Dialog is already added")
        }
    }, 1000)
}

但这显然不是解决方案。

试试 showNow

dialog 除了 show 方法,还有一个 showNow 方法。两者的区别是 show 方法是异步的,showNow 方法是同步的。

kotlin 复制代码
private fun onClick() {
    showDialog()
    showDialog()
}

val dialog = MyDialogFragment()

private fun showDialog() {
    dialog.showNow(supportFragmentManager, "dialog")
}

首先直接调用两次 showNow,仍然会遇到同一个 crash。查看 showNow 方法的源码:

less 复制代码
public void showNow(@NonNull FragmentManager manager, @Nullable String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.setReorderingAllowed(true);
    ft.add(this, tag);
    ft.commitNow();
}

可以看到它和 show 方法的唯一区别是 ft.commit(); 换成了 ft.commitNow(); 同样地,调用时会将 fragment add 一次。所以 Fragment already added 再次出现也是情理之中。

再试试 isAdded

既然 showNow 是同步的,这次 isAdded 总该立刻生效了吧:

kotlin 复制代码
private fun onClick() {
    showDialog()
    showDialog()
}

val dialog = MyDialogFragment()

private fun showDialog() {
    if (dialog.isAdded) {
        Log.d("MainActivity", "Dialog is already added")
        return
    }
    dialog.showNow(supportFragmentManager, "dialog")
}

确实如此,这次可以从 log 中看到,第二次 showNow 时,打出了 log:

csharp 复制代码
Dialog is already added

那么这样就大功告成了吧,天真的我这样想着。

showNow -> dismiss -> showNow

有的读者可能注意到了,谁家 dialog 重复 show 之前,不先调个 dismiss 啊?没错,其实实际工作中的代码是这样的:

kotlin 复制代码
private fun onClick() {
    showDialog()
    dialog.dismiss()
    showDialog()
}

val dialog = MyDialogFragment()

private fun showDialog() {
    if (dialog.isAdded) {
        Log.d("MainActivity", "Dialog is already added")
        return
    }
    dialog.showNow(supportFragmentManager, "dialog")
}

而这种流程下,在 dialog dismiss 之后,showNow 是不会再次展示此 dialog 的:

(我货呢?那么大个 dialog 呢?)

Log 控制台可以看到输出了:

csharp 复制代码
Dialog is already added

这个问题是因为 dismiss 这个方法是异步的。调用 dismiss 之后,isAdded 不会马上变成 false,而是要等一段时间才会变成 false。也就是说这样就没有问题:

kotlin 复制代码
private fun onClick() {
    showDialog()
    dialog.dismiss()
    Handler(Looper.getMainLooper()).postDelayed({
        showDialog()
    }, 1000)
}

val dialog = MyDialogFragment()

private fun showDialog() {
    if (dialog.isAdded) {
        Log.d("MainActivity", "Dialog is already added")
        return
    }
    dialog.showNow(supportFragmentManager, "dialog")
}

运行结果:

可以看到,dismiss 1s 之后,检查 isAdded 这个参数就没有问题了。

但这显然也不是解决方案。作为一个注重用户体验的开发,我不可能让我亲爱的用户楞等一段时间。

到这里就没招了。

总结

  • DialogFragment show/showNow 之后不能直接再调用 show/showNow,否则会报 Fragment already added crash。
  • DialogFragment showNow 之后再 dismiss,下次再想展示需要等 dismiss 执行完。而没人知道多久才能执行完。

所以感觉 DialogFragment 不适合复用。读者如果知晓如何优雅复用 DialogFragment,望不吝赐教。

附 DialogFragment 的 dialog.onDismissListener 不生效

在使用 DialogFragment 时,当 DialogFragment 的 dismiss 方法调用时,给 DialogFragment 的成员变量 dialog 设置的 onDismissListener 是不会被调用的:

kotlin 复制代码
private fun onClick() {
    dialog.showNow(supportFragmentManager, "dialog")
    dialog.dialog?.setOnDismissListener { dialog ->
        Log.d("MainActivity", "Dialog dismissed")
    }
    Handler(Looper.getMainLooper()).postDelayed({
        dialog.dismiss()
    }, 3000)
}

查看 DialogFragment 的源码:

scss 复制代码
public void dismiss() {
    dismissInternal(false, false, false);
}

private void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss, boolean immediate) {
    if (mDismissed) {
        return;
    }
    mDismissed = true;
    mShownByMe = false;
    if (mDialog != null) {
        // Instead of waiting for a posted onDismiss(), null out
        // the listener and call onDismiss() manually to ensure
        // that the callback happens before onDestroy()
        mDialog.setOnDismissListener(null);
        mDialog.dismiss();
        if (!fromOnDismiss) {
            // onDismiss() is always called on the main thread, so
            // we mimic that behavior here. The difference here is that
            // we don't post the message to ensure that the onDismiss()
            // callback still happens before onDestroy()
            if (Looper.myLooper() == mHandler.getLooper()) {
                onDismiss(mDialog);
            } else {
                mHandler.post(mDismissRunnable);
            }
        }
    }
    mViewDestroyed = true;
    if (mBackStackId >= 0) {
        if (immediate) {
            getParentFragmentManager().popBackStackImmediate(mBackStackId,
                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
        } else {
            getParentFragmentManager().popBackStack(mBackStackId,
                    FragmentManager.POP_BACK_STACK_INCLUSIVE, allowStateLoss);
        }
        mBackStackId = -1;
    } else {
        FragmentTransaction ft = getParentFragmentManager().beginTransaction();
        ft.setReorderingAllowed(true);
        ft.remove(this);
        // allowStateLoss and immediate should not both be true
        if (immediate) {
            ft.commitNow();
        } else if (allowStateLoss) {
            ft.commitAllowingStateLoss();
        } else {
            ft.commit();
        }
    }
}

貌似是因为 mDialog.setOnDismissListener(null); 这里将 dialog 的 onDismissListener 设置成了 null 导致的。DialogFragment 官方文档 developer.android.google.cn/reference/a... 里写了 Control of the dialog (deciding when to show, hide, dismiss it) should be done through the APIs here, not with direct calls on the dialog.

这句话的意思是 DialogFragment 里的 dialog 对象不应该被直接操作,而应该通过 DialogFragment 的 API 进行调用,看起来是不推荐直接给它的 dialog 设置 listener。

笔者暂未找到给 DialogFragment 设置 onDismissListener 的替代方案,读者如果知晓,望不吝赐教。

相关推荐
broadview_java30 分钟前
使用 ConstraintLayout 构建自适应界面
android
wy3136228214 小时前
android——开发中的常见Bug汇总与解决方案(闪退)
android·bug
小小测试开发5 小时前
实战派SQL性能优化:从语法层面攻克项目中的性能瓶颈
android·sql·性能优化
QuantumLeap丶5 小时前
《Flutter全栈开发实战指南:从零到高级》- 26 -持续集成与部署
android·flutter·ios
StarShip7 小时前
从Activity.setContentView()开始
android
千里马学框架7 小时前
重学SurfaceFlinger之Layer显示区域bounds计算剖析
android·智能手机·sf·安卓framework开发·layer·surfaceflinger·车载开发
nono牛8 小时前
安卓休眠与唤醒流程
android
二流小码农9 小时前
鸿蒙开发:个人开发者如何使用华为账号登录
android·ios·harmonyos
StarShip9 小时前
Android View框架概览
android·计算机图形学
愤怒的代码9 小时前
解析Android内存分析的指标
android·app