Jetpack Compose - LazyColumn makes an infinite number of recompositions

Viewed 1574

I want to display a list of items using LazyColumn. I wrote the needed code but when I ran the app I observed that the UI is very laggy. I put some Logs to find out where the problem may be and I discovered that the LazyColumn inifinitely recomposes the items. I replaced the LazyColumn with a Column with scrollable modifier and the problem was gone in this case. But I don't know why LazyColumn behaves like it did.

The list is this:

var rides = mutableStateListOf<Ride>(Ride(...), Ride(...))

A list item is like this:

@Stable
@Entity(tableName = "ride_table")
data class Ride(
    var img: Bitmap? = null,
    var path: List<List<LatLng>>? = null,
    var cities: List<String>? = null,
    var timeStarted: Long = 0L,
    var timeEnded: Long = 0L,
    var avgSpeedInKmh: Float = 0f,
    var maxSpeedInKmh: Float = 0f,
    var distanceInMeters: Float = 0f,
    var totalTimeInSeconds: Long = 0L,
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0L
) {
    override fun equals(other: Any?): Boolean {
        if(other == null)
            return false
        if(other is Ride) {
            return id == other.id
        } else {
            return false
        }
    }
}

The list composable:

@Composable
fun Rides(rides: List<Ride>) {
    Log.d("RECOMPOSE", "1")
    LazyColumn {
        items(rides) { ride ->
            Log.d("RECOMPOSE", "2")
            RideDetails(ride)
        }
    }
    /*Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
        rides.forEach { ride ->
            Log.d("RECOMPOSE", "2")
            RideDetails(ride)
        }
    }*/
}

RideDetails:

@Composable
fun RideDetails(ride: Ride) {
    Log.d("RECOMPOSE", "3`")
    Text(text = "ride ${ride}")
}

Log Recompose 2 gets called non stop in the case of LazyColumn. If I switch to Column this doesn't happen and the number of Log calls is the expected one. What am I doing wrong using LazyColumn?

UPDATE (The below Column gets called inside the setContent that lies in onCreateView)

Column {

    val cities = rideViewModel.cities
    val filterCities = rideViewModel.filterCities
        
    val rides = rideViewModel.rides

    TopBar {
        ...
    }

    Chips(cities, filterCities)

    Log.d("RECOMPOSE", "RECOMPOSE INSIDE SCREEN")

    Rides(rides)
}
2 Answers

I've just encountered the same problem with a slightly modified code from the Thinking in Compose Article. In my opinion this is a minimal reproducible as requested by @Philip Dukhov

It gets passed a tiny list of 30 items in this case but the counter goes up endlessly. The LazyColumn displays only the 30 items passed though.

I would be very thankful for an explanation.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SimpleAnimationsTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    var testList = listOf<String>()
                    for(i in 1..30) testList += "Test #$i"
                    ListWithoutBug(myList = testList)
                }
            }
        }
    }
}

@Composable
fun ListWithoutBug(myList: List<String>) { // apparently with bug
    val items = remember { mutableStateOf<Int>(0) }

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        LazyColumn{
            items(myList) { item ->
                Text("Item: $item")
                items.value++
            }
        }
        Text("Count: ${items.value}")
    }
}

Screenshot

The problem was in my XML file.

It was like this:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        android:id="@+id/toolbarWrapper"
        ...
     />

    <TextView
        android:id="@+id/tv_title"
        ...
    />

    <TextView
        android:id="@+id/tv_subtitle"
      ...
    />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:paddingTop="12dp"
        android:paddingBottom="12dp"
        android:layout_marginEnd="20dp"
        android:layout_marginStart="20dp"
        app:layout_constraintBottom_toTopOf="@id/btn_ok"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_subtitle" />

    <ImageButton
        android:id="@+id/btn_ok"
        ...
    />

</androidx.constraintlayout.widget.ConstraintLayout>

To resolve the recompositions of my LazyColumn, I left only the ComposeView on the XML and converted the rest to Composes.

<?xml version="1.0" encoding="utf-8"?>
<androidx.compose.ui.platform.ComposeView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/compose_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
Related