Home > Mobile >  When flow collect stop itself?
When flow collect stop itself?

Time:11-10

There is ParentFragment that shows DialogFragment. I collect a dialog result through SharedFlow. When result received, dialog dismissed. Should I stop collect by additional code? What happens when dialog closed, but fragment still resumed?

// ParentFragment
private fun save() {
    val dialog = ContinueDialogFragment(R.string.dialog_is_save_task)
    dialog.show(parentFragmentManager, "is_save_dialog")

    lifecycleScope.launch {
        dialog.resultSharedFlow.collect {
            when (it) {
                ContinueDialogFragment.RESULT_YES -> {
                    viewModel.saveTask()
                    closeFragment()
                }
                ContinueDialogFragment.RESULT_NO -> {
                    closeFragment()
                }
                ContinueDialogFragment.RESULT_CONTINUE -> {
                    // dont close fragment
                }
            }
        }
    }
}

class ContinueDialogFragment(
    @StringRes private val titleStringId: Int,
    @StringRes private val messageStringId: Int? = null
) : DialogFragment() {

    private val _resultSharedFlow = MutableSharedFlow<Int>(1)
    val resultSharedFlow = _resultSharedFlow.asSharedFlow()

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let { context ->
            AlertDialog.Builder(context)
                .setTitle(getString(titleStringId))
                .setMessage(messageStringId?.let { getString(it) })
                .setPositiveButton(getString(R.string.dialog_yes)) { _, _ ->
                    _resultSharedFlow.tryEmit(RESULT_YES)
                }
                .setNegativeButton(getString(R.string.dialog_no)) { _, _ ->
                    _resultSharedFlow.tryEmit(RESULT_NO)
                }
                .setNeutralButton(getString(R.string.dialog_continue)) { _, _ ->
                    _resultSharedFlow.tryEmit(RESULT_CONTINUE)
                }
                .create()
        } ?: throw IllegalStateException("Activity cannot be null")
    }

    companion object {
        const val RESULT_YES = 1
        const val RESULT_NO = 0
        const val RESULT_CONTINUE = 2
    }
}

CodePudding user response:

I believe you will have a memory leak of DialogFragment: ParentFragment will be referencing the field dialog.resultSharedFlow until the corresponding coroutine finishes execution. The latter may never happen while ParentFragment is open because dialog.resultSharedFlow is an infinite Flow. You can call cancel() to finish the coroutine execution and make dialog eligible for garbage collection:

lifecycleScope.launch {
    dialog.resultSharedFlow.collect {
        when (it) {
            ContinueDialogFragment.RESULT_YES -> {
                viewModel.saveTask()
                closeFragment()
                cancel()
            }
            ContinueDialogFragment.RESULT_NO -> {
                closeFragment()
                cancel()
            }
            ContinueDialogFragment.RESULT_CONTINUE -> {
                // dont close fragment
            }
        }
    }
}

CodePudding user response:

When a Flow completes depends on its original source. A Flow built with flowOf or asFlow() ends once it reaches the last item in its list. A Flow built with the flow builder could be finite or infinite, depending on whether it has an infinite loop in it.

A flow created with MutableSharedFlow is always infinite. It stays open until the coroutine collecting it is cancelled. Therefore, you are leaking the dialog fragment with your current code because you are hanging onto its MutableSharedFlow reference, which is capturing the dialog fragment reference. You need to manually cancel your coroutine or collection.

Or more simply, you could use first() instead of collect { }.


Side note, this is a highly unusual uses of a Flow, which is why you're running into this fragile condition in the first place. A Flow is for a series of emitted objects, not for a single object.

It is also very fragile that you're collecting this flow is a function called save(), but you don't appear to be doing anything in save() to store the instance state such that if the activity/fragment is recreated you'll start collecting from the flow again. So, if the screen rotates, the dialog will reappear, the user could click the positive button, and nothing will be saved. It will silently fail.

DialogFragments are pretty clumsy to work with in my opinion. Anyway, I would take the easiest route and directly put your behaviors in the DialogFragment code instead of trying to react to the result back in your parent fragment. But if you don't want to do that, you need to go through the pain of calling back through to the parent fragment. Alternatively, you could use a shared ViewModel between these two fragments that will handle the dialog results.

  • Related