LazyColumn, LazyRow, LazyVerticalGriditem, items, itemsIndexedmutableStateListOfRecyclerViewColumn composes all of its children, whether they are visible or not. With 5 movies that is
fine, with 5000 search results it is not (slow, memory)RecyclerView (see legacy slide), without the
Adapter/ViewHolder boilerplate// Every item is composed, the Column is just made scrollable
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
movies.forEach { movie -> MovieItem(movie = movie, onMovieClick = {}) }
}
// Only the visible items are composed
LazyColumn {
items(items = movies) { movie -> MovieItem(movie = movie, onMovieClick = {}) }
}
Rule of thumb: a handful of fixed elements -> Column/Row; a list of data -> LazyColumn/LazyRow.
LazyColumn scrolls vertically, LazyRow horizontallyLazyVerticalGrid / LazyHorizontalGrid arrange the items in a grid, the number of columns is
defined by GridCellsGridCells.Fixed(2): exactly two columnsGridCells.Adaptive(minSize = 128.dp): as many columns as fit, each at least 128 dp wideLazyListScope/LazyGridScope block instead of composable childrenLazyRow {
items(items = movies) { movie -> MoviePoster(movie = movie) }
}
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
items(items = movies) { movie -> MoviePoster(movie = movie) }
}
Documentation: Lists and grids
The block of a lazy list is a small DSL (LazyListScope), not a composable:
LazyColumn {
// a single item, e.g. a header
item {
Text(text = "Saved movies", style = MaterialTheme.typography.titleLarge)
}
// one item per element of a list
items(items = movies) { movie ->
MovieItem(movie = movie, onMovieClick = onMovieClick)
}
// the same, with the index
itemsIndexed(items = movies) { index, movie ->
Text(text = "${index + 1}. ${movie.title}")
}
// a fixed number of items
items(count = 3) { index -> Text(text = "Placeholder $index") }
}
items(items = ...) for a List is an extension function: import androidx.compose.foundation.lazy.itemsitem/items calls can be mixed in one list (header, content, footer)By default an item is identified by its position. When the list changes (remove, insert, reorder)
Compose cannot tell which item moved. A key gives every item a stable identity (MovieList.kt of
assignment 5):
@Composable
fun MovieList(movieList: List<Movie>, onMovieClick: (Movie) -> Unit, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
// The key helps Compose to reuse the items when the list changes
items(items = movieList, key = { it.imdbId }) { movie ->
MovieItem(movie = movie, onMovieClick = onMovieClick)
}
}
}
What the key buys:
remember/rememberSaveable state inside an item stays with that item, also after a removal above itModifier.animateItem() can animate insertions, removals and movesIllegalArgumentException: Key "..." was already usedimdbId twice in one result (the OMDb API does): deduplicate the
data (distinctBy { it.imdbId }) before it reaches the listBundle (String, Int, Long, enums, Parcelable), because Compose
restores the item state after the Activity was recreateditems(items = movieList) without key: the Movie data class has no unique
id yet (two Bond movies could have the same title). Assignment 5 introduces the imdbId@Composable
fun MovieList(movieList: List<Movie>, onMovieClick: (Movie) -> Unit, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
LazyColumn(
modifier = modifier,
state = listState,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items = movieList, key = { it.imdbId }) { movie ->
MovieItem(movie = movie, onMovieClick = onMovieClick)
}
}
}
contentPadding pads the content, not the container: the first item gets the top padding, the
last one the bottom padding, and the list still scrolls under the padding (unlike Modifier.padding)Arrangement.spacedBy puts space between the items without a Spacer per itemrememberLazyListState exposes firstVisibleItemIndex and allows programmatic scrolling with
listState.animateScrollToItem(0) (a suspend function, call it from a coroutine, lecture 10)The item composable from the solution of assignment 2 (MainActivity.kt):
@Composable
fun MovieItem(movie: Movie, onMovieClick: (Movie) -> Unit) {
Column(modifier = Modifier
.testTag("movieItem")
.clickable { onMovieClick(movie) }) {
Text(text = movie.title)
Text(text = movie.year)
Text(text = movie.actor)
HorizontalDivider()
}
}
Modifier.clickable { } makes any composable clickable (ripple, accessibility, focus)onMovieClick lambda: the list owns the data, so the list removes the movie (next slide)Modifier.testTag("movieItem") is what the UI test of assignment 2 uses to find the items
(onAllNodesWithTag("movieItem")[0])testTag and clickable apply to the whole ColumnmutableStateListOfRemoving a movie on click (solution of assignment 2):
@Composable
fun MovieList(modifier: Modifier = Modifier) {
val movieList = remember {
mutableStateListOf<Movie>(
Movie("Dr. No", "1962", "Sean Connery"),
Movie("From Russia with Love", "1963", "Sean Connery"),
Movie("Goldfinger", "1964", "Sean Connery")
)
}
LazyColumn(modifier = modifier) {
items(items = movieList) { movie: Movie ->
MovieItem(movie = movie, onMovieClick = { currentMovie: Movie ->
movieList.remove(currentMovie)
})
}
}
}
mutableStateListOf vs. mutableListOfmutableListOf() is a plain
ArrayList: remove() changes its content, but nobody tells Compose, the screen stays as it ismutableStateListOf() returns a SnapshotStateList, a MutableList that Compose observes:
add, remove, clear, ... trigger recomposition of the LazyColumnremember { }, otherwise a new list is created on every recomposition and the removal is lostList in mutableStateOf and replace the whole list:
movies = movies - movie. This is what a ViewModel with StateFlow<List<Movie>> does later (lecture 11)val a = remember { mutableListOf(1, 2, 3) } // a.remove(2) -> no recomposition, UI stale
val b = remember { mutableStateListOf(1, 2, 3) } // b.remove(2) -> LazyColumn recomposes
var c by remember { mutableStateOf(listOf(1, 2, 3)) }
c = c - 2 // new list assigned -> recomposition
Note: changing a property of a Movie inside the list is not observed either, replace the element instead.
items { },
prepare the data before (ViewModel, lecture 11) or use a side effect (lecture 10)Modifier.size(80.dp)): 0-pixel items (an
image before it has loaded) make the list compose far too many items at oncedata class with val properties is stable, Compose can skip
recomposing an item whose Movie did not change. Classes Compose cannot prove stable can be
annotated with @Stable/@ImmutableLazyColumn inside
Column(verticalScroll) throws), use item { } for headers and footers insteadcontentType = { ... } lets Compose reuse compositions between items of the same type in
heterogeneous listsDetails: Lists and grids - performance, Stability in Compose
RecyclerView (View system)LazyColumn:RecyclerView.Adapter creates item views and binds data to them (onCreateViewHolder,
onBindViewHolder, getItemCount)ViewHolder pattern: a holder caches the findViewById() results of one item view; scrolled-out
views are recycled and re-bound with new dataLayoutManager decides the arrangement: LinearLayoutManager (vertical/horizontal),
GridLayoutManager, StaggeredGridLayoutManageritems { } lambdaRecyclerView (or any View) can be shown in Compose with AndroidView (lecture 06)
Assignment 2: Introduction to LazyColumn
What you need from this lecture for it:
LazyColumn with items(items = movieList) { movie -> MovieItem(...) } to show the Bond moviesMovieItem: a Column with three Texts, Modifier.testTag("movieItem") and
Modifier.clickable { onMovieClick(movie) }mutableStateListOf<Movie>() in remember { }, so movieList.remove(movie) in the
click handler recomposes the listLazyColumn, LazyRow, LazyVerticalGrid) compose only the visible items, a Column
composes everythingLazyListScope DSL describes the content with item, items and itemsIndexedkey = { it.imdbId }) keep item state and scroll position correct when the
list changes; duplicate keys crashcontentPadding, Arrangement.spacedBy and rememberLazyListState control layout and scrollingModifier.clickable reports the click, the
owner of the list changes the datamutableStateListOf (or a new List in mutableStateOf) triggers recomposition, a plain
mutableListOf does notRecyclerView + Adapter + ViewHolder is the legacy View-system equivalentLazyColumn preferable to a scrollable Column for a list of search results?item { }, items(list) and itemsIndexed(list)?key in items() buy you, and what happens when two items share a key?contentPadding and Modifier.padding on a LazyColumn?mutableListOf() not update the list on screen?MovieItem get an onMovieClick: (Movie) -> Unit parameter instead of removing the
movie itself?Adapter, ViewHolder and LayoutManager in a RecyclerView, and which part of a
LazyColumn replaces them?