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 key when using animations or dynamic lists
  • Use aspectRatio on images to prevent layout shifts
  • Keep item layouts simple for smooth scrolling
  • Avoid heavy computation in item rendering
  • Use maxLines on text to prevent variable heights

See Also