最近想要复用 DialogFragment 时,遇到一些坑,故写下本文记录。
缘起
导入 DialogFragment:
implementation("androidx.fragment:fragment:1.8.6")
创建一个简单的 DialogFragment:
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 出来:
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 方法:
private fun onClick() {
showDialog()
showDialog()
}
恭喜你遇到这个 Fragment already added crash:
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 方法源码:
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 不就行了,天真的我这样想着:
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。也就是说这样就没有问题:
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 方法是同步的。
private fun onClick() {
showDialog()
showDialog()
}
val dialog = MyDialogFragment()
private fun showDialog() {
dialog.showNow(supportFragmentManager, "dialog")
}
首先直接调用两次 showNow,仍然会遇到同一个 crash。查看 showNow 方法的源码:
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 总该立刻生效了吧:
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:
Dialog is already added
那么这样就大功告成了吧,天真的我这样想着。
showNow -> dismiss -> showNow
有的读者可能注意到了,谁家 dialog 重复 show 之前,不先调个 dismiss 啊?没错,其实实际工作中的代码是这样的:
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 控制台可以看到输出了:
Dialog is already added
这个问题是因为 dismiss 这个方法是异步的。调用 dismiss 之后,isAdded 不会马上变成 false,而是要等一段时间才会变成 false。也就是说这样就没有问题:
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 这个参数就没有问题了。
但这显然也不是解决方案。作为一个注重用户体验的开发,我不可能让我亲爱的用户楞等一段时间。
试试 dismissNow
既然有异步的 show 和同步的 showNow,那么会不会也有异步的 dismiss 和同步的 dismissNow 呢?
试了一下,确实有:
private fun onClick() {
showDialog()
dialog.dismissNow()
showDialog()
}
val dialog = MyDialogFragment()
private fun showDialog() {
if (dialog.isAdded) {
Log.d("MainActivity", "Dialog is already added")
return
}
dialog.showNow(supportFragmentManager, "dialog")
}
这样就没有问题了。
DialogFragment 的 dialog.onDismissListener 不生效
DialogFragment 还有一个坑,当 DialogFragment 的 dismiss 方法调用时,给 DialogFragment 的成员变量 dialog 设置的 onDismissListener 是不会被调用的:
private fun onClick() {
dialog.showNow(supportFragmentManager, "dialog")
dialog.dialog?.setOnDismissListener { dialog ->
Log.d("MainActivity", "Dialog dismissed")
}
Handler(Looper.getMainLooper()).postDelayed({
dialog.dismiss()
}, 3000)
}
查看 DialogFragment 的源码:
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。
通过重写 onDismiss 设置 onDismissListener
为了解决这个问题,可以通过重写 onDismiss 的方式手动实现 onDismissListener 的效果:
class MyDialogFragment : DialogFragment() {
var onDismissListener: (() -> Unit)? = null
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()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismissListener?.invoke()
}
}
总结
- DialogFragment show/showNow 之后不能直接再调用 show/showNow,否则会报 Fragment already added crash。
- DialogFragment 的 show 和 dismiss 函数是异步的,showNow 和 dismissNow 函数是同步的。混用可能会导致问题。
- 给 DialogFragment 的 dialog 设置 onDismissListener 不会起作用,需要通过重写 onDismiss 的方式手动实现 onDismissListener 的效果。