简体   繁体   中英

Jetpack Compose – LazyColumn not recomposing

My LazyColumn is not recomposing but the value is getting updated.

If I scroll down the list and scroll back up I see correct values for the UI

MainActivity

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyTheme {
                MyApp()
            }
        }
    }
}

// Start building your app here!
@Composable
fun MyApp(vm: PuppyListViewModel =  viewModel()) {
    val puppers by vm.pups.collectAsState(emptyList())
    Surface(color = MaterialTheme.colors.background) {
        Column {
            Toolbar()
            LazyColumn {
                items(puppers) { pup ->  PuppyUI(pup, vm::seeDetails, vm::togglePuppyAdoption) }
            }
        }
    }
}

The ViewModel

class PuppyListViewModel : ViewModel() {

    val pups = PuppyRepo.getPuppies().onEach {
        println("FlowEmitted: $it")
    }

    fun togglePuppyAdoption(puppy: Puppy) = viewModelScope.launch {
        PuppyRepo.toggleAdoption(puppy.id)
    }

    fun seeDetails(puppy: Puppy) {
        println("seeDetails $puppy")
    }
}

The model


internal var IDS = 0L

data class Puppy (
    val name: String,
    val tagline: String = "",
    val race: String,
    @DrawableRes val image: Int,
    var adopted: Boolean = false,
    val id: Long = ++IDS,
)

The repository

object PuppyRepo {
    private val changeFlow = MutableStateFlow(0)
    private val pups: List<Puppy>

    private val puppyImages = listOf(
        R.drawable._1,
        R.drawable._2,
        R.drawable._3,
        R.drawable._4,
        R.drawable._5,
        R.drawable._6,
        R.drawable._7,
        R.drawable._8,
        R.drawable._9,
        R.drawable._10,
        R.drawable._11,
        R.drawable._12,
        R.drawable._13,
        R.drawable._14,
        R.drawable._15,
        R.drawable._16,
        R.drawable._17,
        R.drawable._18,
        R.drawable._19,
    )


    private val puppyNames = listOf(
        "Gordie",
        "Alice",
        "Belle",
        "Olivia",
        "Bubba",
        "Pandora",
        "Bailey",
        "Nala",
        "Rosco",
        "Butch",
        "Matilda",
        "Molly",
        "Piper",
        "Kelsey",
        "Rufus",
        "Duke",
        "Ozzy"
    )

    private val puppyTags = listOf(
        "doggo",
        "doge",
        "special dogo",
        "wrinkler",
        "corgo",
        "shoob",
        "puggo",
        "pupper",
        "small dogo",
        "big ol dogo",
        "woofer",
        "floofer",
        "yapper",
        "pupper",
        "good-boye",
        "grizlord",
        "snip-snap dogo"
    )

    private val puppyBreeds = listOf(
        "Labrador Retriever",
        "German Shepard",
        "Golden Retriever",
        "French Bulldog",
        "Bulldog",
        "Beagle",
        "Poodle",
        "Rottweiler",
        "German Shorthaired Pointer",
        "Yorkshire Terrier",
        "Boxer"
    )

    init {
        pups = puppyImages.map { image ->
            val name = puppyNames.random()
            val tagline = puppyTags.random()
            val breed = puppyBreeds.random()
            Puppy(name, tagline, breed, image)
        }
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    fun getPuppies() = changeFlow.flatMapLatest { flowOf(pups) }

    fun getPuppy(puppyId: Long) = flow {
        emit(pups.find { it.id == puppyId })
    }


    suspend fun toggleAdoption(puppyId: Long): Boolean {
        val found = getPuppy(puppyId).first()?.toggleAdoption()?.let { true } ?: false
        if (found) {
            // Trigger a new emission for those that are consuming a Flow from getPuppies
            changeFlow.value = changeFlow.value + 1
        }
        return found
    }


    private fun Puppy.toggleAdoption() {
        adopted = !adopted
    }

}

The Flow pups is producing updated values as you can see in my logcat

日志猫

I've put print statements on my composables and they are not getting re-composed after the flow emits a new value.

Edit.

Lookslike Compose compares the references of the objects and since those didn't change, recomposition didn't happen even if flows were emitting new values (a bug on Compose perhaps?)

Changed the toggle functionality to re-create the instances of the elements of the list like the following and now is working.

Note: I've made Puppy.adopted a val instead of var


suspend fun toggleAdoption(puppyId: Long): Boolean {
    var found = false
    pups = pups.map {
        val isThePuppy = it.id == puppyId
        found = found || isThePuppy
        if(isThePuppy) it.copy(adopted = !it.adopted) else it.copy()
    }
    if (found) {
        // Trigger a new emission for those that are consuming a Flow from getPuppies
        changeFlow.value = changeFlow.value + 1
    }
    return found
}

The Flow pups is producing updated values as you can see in my logcat

Not exactly.

The Flow is emitting the same List of the same Puppy objects. I believe that Compose sees that the List is the same List object as before and assumes that there are no changes.

My suggested changes:

  • Make Puppy be an immutable data class (ie, no var properties)

  • Get rid of changeFlow and have getPuppies() return a stable MutableStateFlow<List<Puppy>> (or make that just be a public property)

  • In toggleAdoption() , create a fresh list of Puppy objects and use that to update the MutableStateFlow<List<Puppy>> :

    suspend fun toggleAdoption(puppyId: Long) {
        val current = puppies.value // assumes that puppies is a MutableSharedFlow<List<Puppy>>

        val replacement = current.map { if (it.id == puppyId) it.copy(adopted = !it.adopted) else it }

        puppies.value = replacement
    }

This worked for me.

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {
    var selectables: List<Selectable> by mutableStateOf(List(100) { Selectable(name = "$it") })
        private set

    fun onTapped(tappedItem: Selectable) {
        val index = selectables.indexOf(tappedItem)
        selectables = selectables.toMutableList().also {
            it[index] = tappedItem.copy(selected = !tappedItem.selected)
        }
    }
}

data class Selectable(
    val name: String,
    var selected: Boolean = false,
)

The key parts are:

  1. Re-assigning the list rather than modifying it in-place (eg making selectables a MutableList and performing selectables[index] = tappedItem.copy(selected =.tappedItem.selected) wouldn't work)
  2. Re-assigning the selected item rather than modifying it in-place, eg the following wouldn't work
selectables = selectables.toMutableList().also {
    it[index].selected = !tappedItem.selected
}

Note that you don't have to make your data class immutable, however, making it immutable will enforce that you have to make a copy of the element in order to update it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM