Flow .map and .collectLatest gets fired unstoppable

Hello,
I have an android application that has a structure like this (although it doesn’t make any difference, it being on android platform. I’m having trouble with flows):

data class MyChild(some stuff){...}

data class MySuper(val child: MyChild, val child2 : List<MyChild2>, val child3 : MyChild3, ...){...}

Now I have a flow like this:

private val _mainState = MutableFlow<MySuper>(MyDefaultSuper)

and in my ViewModels init I have:

fun myInit(child: MyChild){
    //myInit
    Log.d("someTag", "myInit")
    _mainState.update{it.copy(child = child)}
}
CoroutineScope(Dispatchers.IO).launch {
    var tempChild : Child? = null
  _mainState.map{it.child}.collectLatest{
      //1
      Log.d("someTag", "collect Child")
      if(tempChild != it){
          //2
          Log.d("someTag", "tempChild different")
          tempChild = it
      }
     //3
     _mainState.update{
     it.copy(child2 = emptyList())
     //4
     }
     _mainState.udpate{it.copy(cild2 = fetchFromNetwork())}
    
  }
}

Now when i clear out either the part //3 or //4 of the code, it works fine, but when I try do these parts, the log //1 gets fired continuosly, although the part //2 gets called once, so the map and collectLatest don’t work fine even though the child doesn’t changed.
The child only changes in myInit function which is being called once, accoring to the Logs.
Which part of my code is wrong?

What is your expected behavior? For each collect you again emit to the flow, which in turn results in another collect. It does this continuously.

Shouldn’t the map or collectLatest filter out the emits that don’t change the child field?

You may be interested to Flow.distinctUntilChanged

Using a CoroutineScope anonymously (without storing a reference to it) is dangerous, because there’s a chance your entire coroutine gets GC’d while it’s suspended. Either keep a reference to the job or the scope itself.

As I see now, I didn’t provide the information needed in a good way. The line
CoroutineScope(Dispatchers.IO).launch... happens in the init of ViewModel

MutableStateFlow filters out updates that do not change the parent object, however, since you do change child2, this will not block it from emitting. map does not do any filtering of its own, so the downstream flow will still emit whenever the the parent flow does. As mentioned earlier, you can use distinctUntilChanged on the flow produced by map to filter out updates that leave child unchanged

1 Like

Are you sure about this? If we suspend, then someone has to keep a reference to a continuation, because otherwise the coroutine could never resume. I don’t see how GCing the scope would GC the coroutine/continuation as well.

1 Like

If you’re using a ViewModel, then the proper way to launch a coroutine is

viewModelScope.launch(Dispatchers.IO) { ... }

That way the coroutine will be cancelled when the ViewModel goes away.

Just gonna re-iterate what’s already been said; you’re emitting an item to the flow inside the collector. So every time you collect an item, you send a new item. Then you collect the new item, and send another new item. So it’s gonna collect → send → collect → send forever and ever and ever.