28

I'm using Flow instead of LiveData to collect data in my Fragment. In Fragment A I observe (or rather collect) the data in my fragment`s onViewCreated like this:

lifecycleScope.launchWhenStarted {
            availableLanguagesFlow.collect {
                languagesAdapter.setItems(it.allItems, it.selectedItem)
            }
        }

Problem. Then when I go to Fragment B and then comes back to Fragment A, my collect function gets called twice. If I go the Fragment B again and back to A - then collect function is called 3 times. And so on.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Andrew
  • 2,438
  • 1
  • 22
  • 35

2 Answers2

42

Reason

It happens because of tricky Fragment lifecycle. When you come back from Fragment B to Fragment A, then Fragment A gets reattached. As a result fragment's onViewCreated gets called second time and you observe the same instance of Flow second time. Other words, now you have one Flow with two observers, and when the flow emits data, then two of them are called.

Solution 1 for Fragment

Use viewLifecycleOwner in Fragment's onViewCreated. To be more specific use viewLifecycleOwner.lifecycleScope.launch instead of lifecycleScope.launch. Like this:

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
            availableLanguagesFlow.collect {
                languagesAdapter.setItems(it.allItems, it.selectedItem)
            }
        }

Solution 2 for Activity

In Activity you can simply collect data in onCreate.

lifecycleScope.launchWhenStarted {
            availableLanguagesFlow.collect {
                languagesAdapter.setItems(it.allItems, it.selectedItem)
            }
        }

Additional info

  1. Same happens for LiveData. See the post here. Also check this article.
  2. Make code cleaner with Kotlin extension:

extension:

fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner) {
    lifecycleOwner.lifecycleScope.launchWhenStarted {
        this@launchWhenStarted.collect()
    }
}

in fragment onViewCreated:

availableLanguagesFlow
    .onEach {
        //update view
    }.launchWhenStarted(viewLifecycleOwner)

Update

I'd rather use now repeatOnLifecycle, because it cancels the ongoing coroutine when the lifecycle falls below the state (onStop in my case). While without repeatOnLifecycle, the collection will be suspended when onStop. Check out this article.

fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner)= with(lifecycleOwner) {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED){
            try {
                this@launchWhenStarted.collect()
            }catch (t: Throwable){
                loge(t)
            }
        }
    }
}
Andrew
  • 2,438
  • 1
  • 22
  • 35
  • 2
    Thank you dude! I have stucked for fragment for hours! – Ticherhaz FreePalestine Jul 08 '21 at 04:05
  • @Andrew, even with repeatOnLifecycle, it will keep on collecting when fragments comes to STARTED state, right? How did you fix that? – Rakesh Dec 05 '21 at 14:06
  • @Rakesh Correct. The difference is, repeatOnLifecycle starts collecting onStart, cancels collection and all coroutines inside onStop. While without repeatOnLifecycle, starts collecting onStart, and suspend coroutines onStop. I'd recommend you to try both and debug. – Andrew Dec 07 '21 at 09:54
  • 1
    @Andrew, Thanks for the clarification. Is there any way to collect flow only once just like SingleLiveEvent in case of LiveData? – Rakesh Jan 17 '22 at 14:58
  • @Rakesh Yes, it's actually much easier with flow to make the same behaviour as SingleLiveEvent. Take a look at "replay" parameter in Flow. Like MutableSharedFlow(replay = 0). – Andrew Jan 18 '22 at 17:54
  • 1
    Good answer!!! Please mark it as accepted. – Jorge Alejandro Puñales Apr 12 '22 at 18:12
  • This is the correct answer that fixed my crash. lifecycleScope and viewLifecycleOwner.lifecycleScope have subtle differences, as explained in the linked article – Phileo99 Jun 17 '23 at 18:22
1

Use SharedFlow and apply replayCache to it.

Resets the replayCache of this shared flow to an empty state. New subscribers will be receiving only the values that were emitted after this call, while old subscribers will still be receiving previously buffered values. To reset a shared flow to an initial value, emit the value after this call. more information

private val _reorder = MutableSharedFlow<ViewState<ReorderDto?>>().apply {
    resetReplayCache()
}
val reorder: SharedFlow<ViewState<ReorderDto?>>
    get() = _reorder
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83