Compose Lazy Layouts, Lists, Modifiers and Measurement
Compose Lazy Layouts, Lists, Modifiers and Measurement
Lazy Layouts and Lists
1. What is a Lazy Layout?
A lazy layout creates and measures only the items currently needed on screen, plus a small nearby area.
flowchart LR
D["Large dataset"] --> L["Lazy layout"]
L --> V["Compose visible items"]
L --> N["Keep nearby items ready"]
L -.-> O["Do not compose all off-screen items"]This makes lazy layouts suitable for large or unknown-size collections. Android: Lazy lists and grids
---
2. LazyColumn and LazyRow
* LazyColumn: Vertical scrolling list
* LazyRow: Horizontal scrolling list
LazyColumn {
items(products) { product ->
ProductItem(product)
}
}LazyRow {
items(categories) { category ->
CategoryItem(category)
}
}Column vs LazyColumn
Column | LazyColumn |
|---|---|
| Creates all children | Creates required items lazily |
| Does not scroll automatically | Supports vertical scrolling |
| Good for a few items | Good for large lists |
| Uses normal content block | Uses LazyListScope DSL |
Use a normal Column for a small fixed group of UI elements. Use LazyColumn for a large or dynamic list.
---
3. Lazy List DSL
Lazy layouts provide functions such as:
* `item()` for one item
* `items()` for a collection
* `itemsIndexed()` when the index is requiredLazyColumn {
item {
Text("Products")
}
itemsIndexed(products) { index, product ->
Text("${index + 1}. ${product.name}")
}
item {
Text("End of list")
}
}---
4. Lazy Grids
Compose provides:
* LazyVerticalGrid
* LazyHorizontalGrid
Fixed columns
LazyVerticalGrid(
columns = GridCells.Fixed(2)
) {
items(products) { product ->
ProductCard(product)
}
}This always creates two columns.
Adaptive columns
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp)
) {
items(products) { product ->
ProductCard(product)
}
}Compose calculates how many columns fit while keeping each cell at least 150.dp wide.
| Fixed | Adaptive |
|---|---|
| Fixed column count | Column count depends on width |
| Good for predictable designs | Good for phones and tablets |
| Example: exactly two columns | Example: minimum cell width |
---
5. Item Keys
By default, item position helps identify list items. If items are added, deleted or reordered, their positions can change.
Use a stable, unique key:
LazyColumn {
items(
items = products,
key = { product -> product.id }
) { product ->
ProductItem(product)
}
}flowchart LR
C["List changes"] --> K["Stable item IDs"]
K --> M["Compose matches old and new items"]
M --> R["Reuse correct UI and remembered state"]Benefits:
* Preserves item identity
* Helps retain remembered item state
* Supports item movement correctly
* Reduces unnecessary work
Avoid unstable or duplicate keys:
key = { product.name } // Names may repeatPrefer:
key = { product.id }A saveable key should also be compatible with Android saved-state requirements when item state uses rememberSaveable.
---
6. Content Types
A list may contain different UI structures:
Header
Product
Advertisement
Product
FooterUse contentType to tell Compose which items have similar structures:
LazyColumn {
item(
key = "header",
contentType = "header"
) {
ProductHeader()
}
items(
items = products,
key = { it.id },
contentType = { "product" }
) { product ->
ProductItem(product)
}
}Content types help Compose reuse composition more efficiently between compatible items.
Use meaningful categories such as:
header
product
advertisement
loading---
7. List State
LazyListState stores and controls list scrolling information.
val listState = rememberLazyListState()
LazyColumn(
state = listState
) {
items(products) { product ->
ProductItem(product)
}
}It provides values such as:
listState.firstVisibleItemIndex
listState.firstVisibleItemScrollOffset
listState.layoutInfoProgrammatic scrolling
val scope = rememberCoroutineScope()
Button(
scope.launch {
listState.animateScrollToItem(0)
}
}
) {
Text("Go to top")
}Show button after scrolling
val showTopButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}rememberLazyListState() internally supports restoring scroll position through its saver.
---
8. Pagination
Pagination loads data in smaller pages instead of loading everything at once.
flowchart TD
S["User scrolls near end"] --> C{"Already loading?"}
C -->|Yes| W["Wait"]
C -->|No| N["Request next page"]
N --> A["Append new items"]
A --> UI["List updates"]Simple detection
val shouldLoadMore by remember {
derivedStateOf {
val info = listState.layoutInfo
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: 0
lastVisible >= info.totalItemsCount - 3
}
}
LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore) {
viewModel.loadNextPage()
}
}The ViewModel should prevent:
* Duplicate page requests
* Loading when no more pages exist
* Loading while another request is active
Pagination states
data class ProductUiState(
val products: List<Product> = emptyList(),
val isInitialLoading: Boolean = false,
val isLoadingNextPage: Boolean = false,
val paginationError: String? = null,
val endReached: Boolean = false
)For production applications, the Paging library is usually a better solution for large paginated datasets.---
9. Nested Lazy Layouts
Safe: Different scroll directions
A vertical list containing horizontal lists is common:
LazyColumn {
item {
LazyRow {
items(categories) {
CategoryItem(it)
}
}
}
items(products) {
ProductItem(it)
}
}flowchart TD
VC["Vertical LazyColumn"] --> HR["Horizontal LazyRow"]
VC --> P1["Product"]
VC --> P2["Product"]Problem: Same-direction nesting
Avoid an unconstrained vertical LazyColumn inside another vertical LazyColumn.
LazyColumn {
item {
LazyColumn { /* items */ } // Problematic
}
}It can produce infinite-height constraint errors and poor scrolling behaviour.
Better approach
Place everything in one parent lazy list:
LazyColumn {
item {
Header()
}
items(featuredProducts) {
FeaturedProduct(it)
}
item {
Text("All Products")
}
items(allProducts) {
ProductItem(it)
}
}A same-direction nested lazy list may be acceptable when the child has an explicit bounded size, but one list is generally simpler.
---
10. Scrolling Performance
Use these rules:
* Provide stable key values.
* Provide contentType for different item structures.
* Keep item composables small.
* Avoid expensive work inside items.
* Cache genuinely expensive calculations.
* Use immutable item models.
* Avoid nesting same-direction scrolling containers.
* Do not pass the complete list state to every item.
* Load properly sized images.
* Avoid creating unnecessary objects during fast scrolling.
* Measure performance in a release build.
Avoid
items(products) { product ->
val bitmap = loadLargeBitmap(product.image) // Expensive
ProductItem(product, bitmap)
}Move expensive loading outside composition or use an appropriate asynchronous image-loading solution.
---
Modifiers, Layout and Measurement
11. What is a Modifier?
A Modifier changes or adds behaviour to a composable.
It can control:
* Size
* Padding
* Background
* Drawing
* Click handling
* Scrolling
* Alignment
* Accessibility
* Pointer input
Text(
text = "Hello",
modifier = Modifier
.fillMaxWidth()
.background(Color.Blue)
.padding(16.dp)
)Modifiers form an ordered, immutable chain. Android: Compose modifiers
---
12. Modifier Order
Modifier order changes the result.
Padding outside background
Modifier
.padding(16.dp)
.background(Color.Blue)Outer padding → Blue backgroundPadding inside background
Modifier
.background(Color.Blue)
.padding(16.dp)Blue background → Inner paddingflowchart LR
M1["First modifier"] --> M2["Second modifier"]
M2 --> M3["Third modifier"]
M3 --> C["Composable"]Clickable order
Modifier
.clickable { }
.padding(16.dp)The padded area is included in the clickable region.
Modifier
.padding(16.dp)
.clickable { }The outer padding is generally outside the clickable region.
> Read modifier chains from left to right: each modifier wraps the result produced after it.
---
13. Constraints
A constraint tells a child the minimum and maximum size it may use.
Minimum width
Maximum width
Minimum height
Maximum heightflowchart TD
P["Parent"] -->|Send constraints| C["Child"]
C -->|Choose permitted size| P
P -->|Place child| UI["Final layout"]The key layout rule is:
> Parents provide constraints, children choose a permitted size, and parents place children.
For example:
Parent allows width: 0–300 px
Child chooses width: 200 px
Parent places the 200 px child---
14. Measurement and Placement
Compose layout normally works in three main steps:
flowchart LR
C["1. Parent sends constraints"] --> M["2. Child measures"]
M --> P["3. Parent places child"]Measurement
The child chooses a size inside the received constraints.
Placement
The parent decides the child’s x and y position.
This occurs during the layout phase, after composition.
Composition → Measurement → Placement → DrawingCompose normally requires each child to be measured once in a measurement pass. Intrinsic measurement APIs provide special cases but can add cost.
---
15. Size Behaviour
Preferred size
Modifier.size(100.dp)This requests 100.dp, but parent constraints still have final control.
If the parent permits only `60.dp`, the child cannot normally become `100.dp`.Required size
Modifier.requiredSize(100.dp)This strongly forces the child’s measured size, even when it conflicts with incoming constraints. The layout system may then position the child within the size reported to its parent, so it should be used carefully.
Fill available space
Modifier.fillMaxWidth()
Modifier.fillMaxHeight()
Modifier.fillMaxSize()Limit size
Modifier
.widthIn(min = 100.dp, max = 300.dp)
.heightIn(max = 200.dp)---
16. Padding Behaviour
padding() changes the constraints passed to the child and then adds space around it.
Modifier
.fillMaxWidth()
.padding(16.dp)Simplified example:
Available width = 300 dp
Horizontal padding = 16 + 16 dp
Child receives approximately 268 dp
Final occupied width = 300 dpModifier order decides whether padding is:
* Inside a background
* Outside a background
* Clickable
* Not clickable
---
17. Custom Layout
Use the Layout composable when standard layouts such as Row, Column and Box cannot provide the required measurement and placement.
Example: place children vertically with custom spacing.@Composable
fun CustomVerticalLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
val width = placeables.maxOfOrNull {
it.width
} ?: 0
val height = placeables.sumOf {
it.height
}
layout(
width = width.coerceIn(
constraints.minWidth,
constraints.maxWidth
),
height = height.coerceIn(
constraints.minHeight,
constraints.maxHeight
)
) {
var yPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(
x = 0,
y = yPosition
)
yPosition += placeable.height
}
}
}
}Internal flow:
flowchart TD
M["Receive measurable children"] --> MC["Measure with constraints"]
MC --> S["Calculate layout size"]
S --> L["Call layout"]
L --> P["Place each child"]---
18. Drawing Modifiers
Drawing happens after layout size and position are known.
Common drawing modifiers include:
Modifier.background(Color.Blue)
Modifier.border(1.dp, Color.Black)
Modifier.drawBehind { }
Modifier.drawWithContent { }
Modifier.graphicsLayer { }drawBehind
Draws behind the composable content:
Modifier.drawBehind {
drawCircle(
color = Color.Yellow,
radius = size.minDimension / 2
)
}drawWithContent
Controls when the original content is drawn:
Modifier.drawWithContent {
drawContent()
drawRect(
color = Color.Black.copy(alpha = 0.2f)
)
}flowchart LR
L["Layout complete"] --> D["Drawing modifiers"]
D --> C["Draw content"]
C --> S["Pixels on screen"]Avoid expensive object creation inside drawing blocks because they may run frequently.
---
19. Pointer Input
Use pointerInput for low-level gesture handling.
Modifier.pointerInput(Unit) {
detectTapGestures(
// Handle long press
}
)
}Use standard high-level modifiers when possible:
Modifier.clickable { }
Modifier.scrollable(...)
Modifier.draggable(...)Use pointerInput when custom gesture behaviour is required.
Pointer input keys
Modifier.pointerInput(enabled) {
if (enabled) {
detectTapGestures { /* Handle tap */ }
}
}When enabled changes, the old pointer-input coroutine is cancelled and restarted.
---
20. Modifier Reuse
Modifier chains are immutable and safe to reuse.
val productModifier = Modifier
.fillMaxWidth()
.padding(12.dp)LazyColumn {
items(products) { product ->
ProductItem(
product = product,
modifier = productModifier
)
}
}Benefits:
* Less repeated allocation
* Easier modifier comparison
* Consistent styling
* Cleaner code
Reusable modifiers can be especially helpful in frequently recomposing UI and lazy-list items.
Scoped modifiers
Some modifiers only work in a specific parent scope:
Column {
Text(
text = "Title",
modifier = Modifier.weight(1f)
)
}weight() is a ColumnScope or RowScope modifier. Do not move it outside the required scope.
---
Common Mistakes
Lazy Layout Mistakes
* Using Column for thousands of items
* Missing stable item keys
* Using duplicate keys
* Ignoring contentType for very different item layouts
* Starting duplicate pagination requests
* Nesting unconstrained lists in the same direction
* Performing expensive work inside list items
* Reading fast-changing list state directly in a large parent scope
Modifier and Layout Mistakes
* Assuming modifier order does not matter
* Using `requiredSize()` without understanding constraints
* Measuring a child repeatedly in custom layout code
* Forgetting to respect parent constraints
* Putting expensive work inside drawing or pointer-input loops
* Using `pointerInput(Unit)` when changing data should restart it
* Applying scope-specific modifiers in the wrong parent
* Recreating long identical modifier chains unnecessarily---
Interview Questions and Answers
Q1. Why is LazyColumn better for large lists?
It composes and lays out only the items required for the visible viewport instead of creating every item immediately.
Q2. What is the difference between LazyColumn and Column?
Column creates all children and does not scroll automatically. LazyColumn supports scrolling and creates items lazily.
Q3. Why do we provide item keys?
Keys preserve item identity during insertions, deletions and reordering. They also help retain the correct remembered item state.
Q4. What is contentType?
It identifies items with similar composition structures so Compose can reuse compatible item content more efficiently.
Q5. What does LazyListState provide?
It provides current scroll information and functions such as scrollToItem() and animateScrollToItem().
Q6. How do you implement pagination?
Observe when the user approaches the end, request the next page once, append its items and track loading, error and end-reached states.
Q7. Can we nest lazy lists?
Different-direction nesting is common. Avoid an unconstrained lazy list inside another lazy list scrolling in the same direction.
Q8. Why does Modifier order matter?
Every modifier wraps or changes the result of the following modifier, so order affects size, padding, drawing and input behaviour.
Q9. What are constraints?
Constraints are the minimum and maximum width and height that a parent allows its child to use.Q10. Explain Compose measurement.
The parent sends constraints, the child chooses an allowed size, and the parent places the measured child.
Q11. What is the difference between size() and requiredSize()?
size() requests a preferred size while respecting parent constraints. requiredSize() forces the requested child size more strongly and should be used carefully.
Q12. When should we create a custom layout?
Use a custom layout when standard Row, Column, Box or other layouts cannot provide the required measurement and placement behaviour.
Q13. When should we use pointerInput?
Use it for custom or low-level gesture handling. Prefer standard modifiers such as clickable for common interactions.
Q14. Why reuse Modifier chains?
Reuse can reduce allocation and comparison work, improve consistency and keep code readable.
---
Final Interview Answer
> Compose lazy layouts create only the items needed around the visible area. LazyColumn and LazyRow support vertical and horizontal lists, while lazy grids support fixed or adaptive cells. Stable keys preserve item identity, content types improve reuse, and LazyListState controls and observes scrolling. For performance, I avoid expensive item work, duplicate pagination and unbounded same-direction nested lists. In Compose layout, the parent sends constraints, the child chooses an allowed size, and the parent places it. Modifier order affects measurement, drawing and input, so padding, size, background and click modifiers can behave differently when reordered.