Documentation
Lazy Lists
Efficiently display large lists with LazyColumn, LazyRow, and LazyVerticalGrid.
LazyColumn
Vertical scrolling list that only renders visible items:
<LazyColumn gap={12}>
@for (item in items) {
<Card>
<Text>{item.name}</Text>
</Card>
}
</LazyColumn> The @for loop inside a lazy list automatically transpiles to items().
Single direct children are wrapped in item {}.
LazyRow
Horizontal scrolling list:
<LazyRow gap={8}>
@for (category in categories) {
<Card w={120}>
<Image src={category.icon} />
<Text>{category.name}</Text>
</Card>
}
</LazyRow> LazyVerticalGrid
Grid layout with multiple columns:
<LazyVerticalGrid columns={2} gap={12}>
@for (product in products) {
<Card>
<Image src={product.image} />
<Text>{product.name}</Text>
<Text>${product.price}</Text>
</Card>
}
</LazyVerticalGrid> Responsive Columns
<LazyVerticalGrid columns={3} gap={8}>
@for (photo in photos) {
<Image src={photo.url} aspectRatio={1.0} />
}
</LazyVerticalGrid> Spacing
Use gap for consistent spacing between items:
<LazyColumn gap={16}> <!-- 16dp between items -->
@for (item in items) {
<Text>{item}</Text>
}
</LazyColumn> Content Padding
Add padding around the entire list:
<LazyColumn gap={12} p={16}>
@for (item in items) {
<Card>{item.name}</Card>
}
</LazyColumn> Empty State
Show a message when the list is empty:
<LazyColumn>
@for (item in items) {
<Card>{item.name}</Card>
} empty {
<Column fillMaxSize horizontalAlignment="center" verticalArrangement="center">
<Icon name="inbox" size={48} color="secondary" />
<Text color="secondary">No items yet</Text>
</Column>
}
</LazyColumn> Pull to Refresh
var isRefreshing = false
suspend fun refresh() {
isRefreshing = true
items = $fetch("https://api.example.com/items")
isRefreshing = false
}
<LazyColumn pullRefresh onRefresh={refresh} refreshing={isRefreshing}>
@for (item in items) {
<Card>{item.name}</Card>
}
</LazyColumn> Sticky Headers
<LazyColumn>
@for (group in groupedItems) {
<Text sticky fontWeight="bold" background="#F5F5F5" p={12}>
{group.category}
</Text>
@for (item in group.items) {
<Card>{item.name}</Card>
}
}
</LazyColumn> Item Animation
Use animate:flip for reordering animations:
<LazyColumn gap={8}>
@for (item in items) {
<Card key={item.id} animate:flip={{ duration: 200 }}>
<Text>{item.name}</Text>
</Card>
}
</LazyColumn> Examples
News Feed
var articles = []
$onMount { launch { articles = $fetch("https://api.example.com/articles") } }
<LazyColumn gap={16} p={16}>
@for (article in articles) {
<Card onClick={() => $navigate("/article/{article.id}")}>
<Image src={article.image} h={200} />
<Column p={16} gap={8}>
<Text fontSize={18} fontWeight="bold">{article.title}</Text>
<Text fontSize={14} color="secondary" maxLines={2}>
{article.summary}
</Text>
</Column>
</Card>
}
</LazyColumn> Product Grid
var products = []
<LazyVerticalGrid columns={2} gap={12} p={16}>
@for (product in products) {
<Card onClick={() => $navigate("/product/{product.id}")}>
<Image
src={product.image}
aspectRatio={1.0}
transition:crossfade="product-{product.id}"
/>
<Column p={12} gap={4}>
<Text fontSize={14} maxLines={2}>{product.name}</Text>
<Text fontSize={16} fontWeight="bold">${product.price}</Text>
</Column>
</Card>
}
</LazyVerticalGrid> Horizontal Category List
<Column gap={16}>
<Text fontSize={20} fontWeight="bold" px={16}>Categories</Text>
<LazyRow gap={12} px={16}>
@for (category in categories) {
<Card w={100} onClick={() => selectCategory(category.id)}>
<Image src={category.icon} w={100} h={100} />
<Text fontSize={12} textAlign="center" p={8}>
{category.name}
</Text>
</Card>
}
</LazyRow>
</Column> Photo Gallery
var photos = []
<LazyVerticalGrid columns={3} gap={2}>
@for (photo in photos) {
<Image
src={photo.thumbnail}
aspectRatio={1.0}
onClick={() => $navigate("/photo/{photo.id}")}
transition:crossfade="photo-{photo.id}"
/>
}
</LazyVerticalGrid> Contacts List with Headers
var contacts = groupContactsByLetter()
<LazyColumn>
@for (group in contacts) {
<Text
sticky
fontSize={14}
fontWeight="bold"
background="#F5F5F5"
px={16} py={8}
>
{group.letter}
</Text>
@for (contact in group.contacts) {
<Row
gap={12}
p={16}
onClick={() => $navigate("/contact/{contact.id}")}
>
<Image
src={contact.avatar}
w={48} h={48}
cornerRadius={24}
/>
<Column>
<Text fontSize={16}>{contact.name}</Text>
<Text fontSize={14} color="secondary">{contact.phone}</Text>
</Column>
</Row>
}
}
</LazyColumn> Search Results
var query = ""
var results = []
suspend fun search() {
results = $fetch.get(
url = "https://api.example.com/search",
params = mapOf("q" to query)
)
}
<Column fillMaxSize>
<TextField
bind:value={query}
placeholder="Search..."
leadingIcon="search"
onSubmit={search}
p={16}
/>
<LazyColumn gap={12} p={16}>
@for (result in results) {
<Card onClick={() => $navigate("/item/{result.id}")}>
<Row gap={12} p={12}>
<Image src={result.thumbnail} w={60} h={60} />
<Column weight={1}>
<Text fontSize={16} fontWeight="medium">{result.title}</Text>
<Text fontSize={14} color="secondary" maxLines={2}>
{result.description}
</Text>
</Column>
</Row>
</Card>
} empty {
<Column fillMaxSize horizontalAlignment="center" pt={60}>
<Icon name="search-off" size={48} color="secondary" />
<Text color="secondary">No results found</Text>
</Column>
}
</LazyColumn>
</Column> Performance Tips
- Always provide a
keywhen using animations or dynamic lists - Use
aspectRatioon images to prevent layout shifts - Keep item layouts simple for smooth scrolling
- Avoid heavy computation in item rendering
- Use
maxLineson text to prevent variable heights
See Also
- animate:flip - List reordering animations
- Layout Components - Column, Row, Box
- Image Component - Displaying images in lists