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 的替代方案,读者如果知晓,望不吝赐教。

相关推荐
斗锋在干嘛3 小时前
Android里面内存优化
android
jiet_h4 小时前
深入解析Kapt —— Kotlin Annotation Processing Tool 技术博客
android·开发语言·kotlin
alexhilton5 小时前
实战:探索Jetpack Compose中的SearchBar
android·kotlin·android jetpack
uhakadotcom5 小时前
EventBus:简化组件间通信的利器
android·java·github
笑鸿的学习笔记6 小时前
ROS2笔记之服务通信和基于参数的服务通信区别
android·笔记·microsoft
8931519607 小时前
Android开发融云获取多个会话的总未读数
android·android开发·android教程·融云获取多个会话的总未读数·融云未读数
zjw_swun8 小时前
实现了一个uiautomator玩玩
android
pengyu8 小时前
系统化掌握Dart网络编程之Dio(二):责任链模式篇
android·flutter·dart
水w8 小时前
【Android Studio】如何卸载干净(详细步骤)
android·开发语言·android studio·activity
亦是远方8 小时前
2025华为软件精英挑战赛2600w思路分享
android·java·华为