Home > database >  MutableSharedFlow of list unexpectedly updates a separate plain Kotlin list
MutableSharedFlow of list unexpectedly updates a separate plain Kotlin list

Time:11-11

this code runs on desktop using compose multiplatform but I guess it would work on android the same way.

class MyData{
  var x: Int = -1
}

object Repo {
  val allData = (0..10).map { MyData().apply { x=it } }
  val mySharedFlow = MutableSharedFlow<List<MyData>>(1)
}
fun main() = runBlocking {
  // initialize with some data from `Repo.allData`
  Repo.mySharedFlow.emit(Repo.allData.take(5))
  application {
     Window(onCloseRequest = ::exitApplication) {
        MainScreen()
     }
  }
}
@Composable
fun MainScreen() {
  val currlist = Repo.mySharedFlow.collectAsState(emptyList())
  val scope = rememberCoroutineScope()
  Column {
     currlist.value.forEach { Text(it.x.toString()) }

     Button({
        scope.launch {
          val newList = currlist.value.toMutableList()
          newList[0].x = 500 // changes the original `allData`
//        newList[0] = newList[0].apply { x=500 } // changes the original `allData`
//        newList[0] = MyData().apply { x=500 } // Does not change the original `allData`

          Repo.mySharedFlow.emit(newList.plus(MyData())) //optional: added new element just to force recomposition
        }
     }) { Text("modify list") }
     
     // A button to print `allData`
     Button({ scope.launch { println(Repo.allData.map { it.x }) } }) { Text("print allData") }

  }
}

as you can see when I click "modify list" button I'm trying to modify an item in the composable state currlist but when I check the original list Repo.allData I find that it changed as well !! why ? I also noticed that using newList[0] = MyData().apply { x=500 } doesn't cause Repo.allData to change ! , this is weird for me, I thought that when I initialized the flow with Repo.mySharedFlow.emit(Repo.allData.take(5)) I was creating a completely new list indepedent from Repo.allData , I hope I formulated the issue correctly, can some one explain to me why allData changes in the first place and then why it didn't change when using newList[0] = MyData().apply { x=500 } ?

Edit: explained by @Tenfour04 , thanks, it had nothing to do with flows:

  println(Repo.allData.map { it.x })
//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  val mlist = Repo.allData.take(5).toMutableList()
  mlist[0].x=500
  println(Repo.allData.map { it.x })
//[500, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

CodePudding user response:

Although you copied the list using toMutableList(), it wasn’t a deep copy. Both old and new lists are referencing (pointing at) the same item instances. You are mutating one of those shared item instances.

When you call the constructor you put a new item instance in the new list, so you are not mutating a shared item instance.

Side note, you should avoid ever putting a mutable class (one with any var properties) in a StateFlow. That is very error prone. StateFlow is always comparing old and new values when it gets new values, so it will get messed up if the old value is getting modified out from under it.

  • Related