DialogFragment 打开时不执行 RepeatOnLifecycle

RepeatOnLifecycle not executed when DialogFragment is open

我有一个 Fragment,它打开一个 DialogFragment,里面有一些设置。当我更改 DialogFragment 中的设置时,我希望底层 Fragment 收集更改。 我使用视图模型和 StateFlows 来存储设置并在片段中设置收集器,但是,它们从未被调用。但是视图模型已正确更新。 是否因为显示 DialogFragment 而未调用收集器的 viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED)?但是背景片段还是活跃的

当我使用 MutableLiveData 而不是 MutableStateFlow 并创建一个观察者而不是收集器时,它起作用了。所以一定和repeatOnLifecycle有关。

class SomeFragment : Fragment() {
    // Dropped the other functions, e.g. onCreate, for simplicity

    val settings by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.testButton.setOnClickListener {
            val settingsFragment = SettingsDialogFragment()
            settingsFragment.show(requireActivity().supportFragmentManager, "settings")
        }

        lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                settings.lowerState.collect {
                    Log.e("DEBUG", "lowerState switched")
                }
                settings.upperState.collect {
                    Log.e("DEBUG", "upperState switched")
                }
            }
        }
    }
}
class SettingsViewModel(application: Application) : AndroidViewModel(application) {

    val lowerState: MutableStateFlow<Boolean> = MutableStateFlow(true)
    val upperState: MutableStateFlow<Boolean> = MutableStateFlow(true)
}

对话框片段是一个没有特殊功能的简单对话框

class SettingsDialogFragment : DialogFragment() {

    private var _binding: FragmentSettingsDialogBinding? = null
    private val binding get() = _binding!!

    private val settings: SettingsViewModel by activityViewModels()

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        _binding = FragmentRecordingSettingsDialogBinding.inflate(
            layoutInflater,
            null,
            false
        )
        _binding!!.settings = settings

        val builder = AlertDialog.Builder(requireActivity())

        return builder.setView(binding.root).create()
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="settings"
            type="com.example.SettingsViewModel"/>
    </data>
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <androidx.appcompat.widget.SwitchCompat
                android:checked="@={settings.upperState}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
            <androidx.appcompat.widget.SwitchCompat
                android:checked="@={settings.lowerState}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
        </LinearLayout>
</layout>

DialogFragments 的情况下,有时可能没有视图。对于 DialogFragments,您应该使用 lifecycleOwner.

在幕后 repeatOnLifecycle 挂起调用协程,当生命周期在新协程中进出目标状态时重新启动块,并恢复调用协程当 生命周期被销毁时。所以调用 repeatOnLifecycle 的协程直到生命周期被销毁后才会恢复执行。

此处可能的解决方案之一是将 Lifecycle.State.RESUMED 生命周期 而不是 viewLifecycleOwner

 lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
            // Repeat when the lifecycle is RESUMED, cancel when PAUSED
        }

采用了您的代码 - 只是准系统,我能够让它工作。 您可以看到 SomeFragment 中收集的流中有日志记录,这些日志由开关更改:

想法:

我假设通过使用委托 by activityViewModels() 让你的 SettingsViewModel 两个片段有一个共同的父 Activity,因此提供了相同的视图模型实例。

您的 collect { .. } 尾调用正在挂起,因此无论如何您将永远只能让第一个收集器收集,在这种情况下:

settingsViewModel.lowerState
        .collect { .. suspending block in this coroutine nothing passed this will execute as FlowCollector still "collecting" and pushing into this block .. }

同一 suspend CoroutineScope.() -> Unit 块中多个流的正确语法类似于:

 settingsViewModel.lowerState
        .onEach { Log.e("DEBUG", "lowerState switched") }
        .launchIn(this)
 settingsViewModel.upperState
        .onEach { Log.e("DEBUG", "upperState switched") }
        .launchIn(this)

这种方式 launchIn() 是尾部调用 scope.launch { collect() } 的捷径 - 这会启动一个没有 suspend 块的新协程 - 由 onEach { .. } 处理。

除此之外没什么好说的,也许可以使用您自己的准系统代码并尝试在一个新项目中消除此逻辑作为根本问题。由于所有“帮助”函数 Android 围绕作用域/生命周期创建,它确实使该代码更难单独测试,因为它依赖于框架 类 和 LifeCycleOwner 回调和恕我直言使其更难理解 - 但是.. 只是我的意见和我在复制和查看代码时想到的一些事情。

您好,请尝试使用此代码,因为当对话框关闭时恢复状态将调用 somefragment。

lifecycleScope.launch {
                viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
                    settings.lowerState.collect {
                        Log.e("DEBUG", "lowerState switched")
                    }
                    settings.upperState.collect {
                        Log.e("DEBUG", "upperState switched")
                    }
                }
            }