KtDevLog
  • Home
  • Jetpack Compose
  • Kotlin Fundamentals
  • Android Studio
No Result
View All Result
KtDevLog
  • Home
  • Jetpack Compose
  • Kotlin Fundamentals
  • Android Studio
No Result
View All Result
KtDevLog
No Result
View All Result
Jetpack Compose LazyColumn Example: Build Fast Scrolling Lists

Jetpack Compose LazyColumn Example: Build Fast Scrolling Lists

Md Sharif Mia by Md Sharif Mia
May 1, 2026
in Jetpack Compose
0
1
Share on FacebookShare on PinterestShare on X

Every Android app you’ve ever used has a list in it. Contacts, messages, products, news feeds, settings — lists are everywhere. And for years, building them meant one thing: RecyclerView.

RecyclerView worked. But “worked” is doing a lot of heavy lifting in that sentence. You needed an Adapter, a ViewHolder, a layout file for each item, an item layout XML, a DiffUtil callback, and a dozen lines of boilerplate just to show a list of strings.

In Jetpack Compose, you do it differently. This Jetpack Compose LazyColumn example guide shows you how to build the same fast, efficient scrolling lists — with images, text, click handlers, and proper performance — in a fraction of the code. No Adapter. No ViewHolder. No XML.

Related Posts

Remember vs rememberSaveable in Jetpack Compose Explained

Remember vs rememberSaveable in Jetpack Compose Explained

May 2, 2026
Jetpack Compose Navigation Tutorial: Pass Arguments Easily

Jetpack Compose Navigation Tutorial: Pass Arguments Easily

April 29, 2026
Row in Jetpack Compose: 5 Essential Layout Tips

Row in Jetpack Compose: 5 Essential Layout Tips

April 23, 2026
Your First Jetpack Compose Function Explained

Your First Jetpack Compose Function Explained

April 21, 2026

Let’s build it from scratch.

Table of Contents

  • What Is LazyColumn in Jetpack Compose?
  • Your First Jetpack Compose LazyColumn Example
  • Building a Real List Item — Text and Image Together
  • The item key Parameter — Most Important LazyColumn Detail
  • Adding a List Header
  • Sticky Headers — Group Your List
  • Scroll to Top Button — Using rememberLazyListState
  • Performance Rules — What Not to Do
    • ❌ Don’t Sort Inside LazyColumn
  • LazyColumn vs RecyclerView — The Real Comparison
  • Frequently Asked Questions
    • What is LazyColumn in Jetpack Compose?
    • What is the difference between Column and LazyColumn in Jetpack Compose?
    • Why should I use item keys in LazyColumn?
    • How do I add images to LazyColumn items in Jetpack Compose?
    • Is LazyColumn better than RecyclerView?
  • Conclusion

What Is LazyColumn in Jetpack Compose?

LazyColumn is Jetpack Compose’s answer to RecyclerView. It displays a vertically scrolling list of items — but crucially, it only composes and lays out the items that are currently visible on screen.

Think of a restaurant with 200 menu items. The waiter doesn’t bring all 200 to your table at once. They bring what’s in front of you, and swap items in as you look further down. LazyColumn works exactly the same way — it keeps your app fast and memory-efficient regardless of how many items are in your list.

According to the official Jetpack Compose documentation, LazyColumn provides a vertically scrolling list that composes only visible items, which is why it’s recommended for any list longer than a handful of items.

The difference between Column and LazyColumn is stark:

ColumnLazyColumn
Renders itemsAll at onceOnly visible items
Large listsSlow — high memory useFast — efficient
ScrollingNot built inBuilt in automatically
Use case2–5 fixed itemsAny dynamic list

If you have more than about 5 items, always reach for LazyColumn.

Your First Jetpack Compose LazyColumn Example

Let’s start with the simplest possible working example — a list of strings:

Kotlin
@Composable
fun SimpleList() {
    val items = listOf(
        "Kotlin Fundamentals",
        "Jetpack Compose",
        "StateFlow & SharedFlow",
        "Navigation Compose",
        "Data Classes",
        "Extension Functions",
        "Sealed Classes",
        "Null Safety",
        "Coroutines",
        "Android Studio Gemini AI"
    )

    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(items) { item ->
            Text(
                text = item,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 8.dp),
                fontSize = 16.sp
            )
        }
    }
}
Kotlin

Three things to notice immediately:

contentPadding = PaddingValues(16.dp) adds padding around the entire list — including the first and last item — without clipping the scroll edges. This is different from adding padding directly to the LazyColumn modifier, which would clip content at the edges during scrolling.

verticalArrangement = Arrangement.spacedBy(8.dp) adds consistent space between every item automatically. No manual Spacer() calls.

items(items) { item -> } is the LazyColumn DSL. You pass your list and receive each item in the lambda. That lambda is your item composable — whatever you put in here is what each row looks like.

Building a Real List Item — Text and Image Together

The simple string list is a starting point. Real apps need richer list items — an image, a title, a subtitle, maybe a badge. Here’s how to build a proper card-style list item with an image and text:

First, define your data model using a Kotlin data class:

Kotlin
data class Course(
    val id: Int,
    val title: String,
    val description: String,
    val imageUrl: String,
    val duration: String
)
Kotlin

Now create the sample data:

Kotlin
val courses = listOf(
    Course(1, "Kotlin Fundamentals", "Master val, var, data classes and more", "https://ktdevlog.com/img/kotlin.png", "2h 30m"),
    Course(2, "Jetpack Compose UI", "Build modern Android UIs declaratively", "https://ktdevlog.com/img/compose.png", "4h 15m"),
    Course(3, "StateFlow & Coroutines", "Reactive state management in Android", "https://ktdevlog.com/img/flow.png", "3h 00m"),
    Course(4, "Navigation Compose", "Multi-screen apps with argument passing", "https://ktdevlog.com/img/nav.png", "1h 45m"),
    Course(5, "Sealed Classes", "Model UI state the professional way", "https://ktdevlog.com/img/sealed.png", "1h 20m"),
)
Kotlin

Now build the item composable:

Kotlin
@Composable
fun CourseItem(
    course: Course,
    onClick: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onClick() },
        shape = RoundedCornerShape(12.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
        colors = CardDefaults.cardColors(containerColor = Color.White)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            // Course image
            AsyncImage(
                model = course.imageUrl,
                contentDescription = course.title,
                modifier = Modifier
                    .size(64.dp)
                    .clip(RoundedCornerShape(8.dp)),
                contentScale = ContentScale.Crop
            )

            // Text content
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = course.title,
                    fontSize = 16.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color(0xFF1E293B)
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = course.description,
                    fontSize = 13.sp,
                    color = Color(0xFF64748B),
                    maxLines = 2,
                    overflow = TextOverflow.Ellipsis
                )
                Spacer(modifier = Modifier.height(6.dp))
                // Duration badge
                Surface(
                    shape = RoundedCornerShape(4.dp),
                    color = Color(0xFFEDE9FE)
                ) {
                    Text(
                        text = course.duration,
                        modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
                        fontSize = 11.sp,
                        color = Color(0xFF7C3AED),
                        fontWeight = FontWeight.Medium
                    )
                }
            }
        }
    }
}
Kotlin

AsyncImage from the Coil library loads remote images asynchronously with automatic caching — the standard choice for image loading in Compose apps in 2026.

Now wire it together with LazyColumn:

Kotlin
@Composable
fun CourseList(
    courses: List<Course>,
    onCourseClick: (Course) -> Unit
) {
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFFF8FAFC)),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(
            items = courses,
            key = { course -> course.id }  // ← Critical for performance
        ) { course ->
            CourseItem(
                course = course,
                onClick = { onCourseClick(course) }
            )
        }
    }
}
Kotlin

The item key Parameter — Most Important LazyColumn Detail

That key = { course -> course.id } line deserves its own section — because it’s the single most impactful thing you can do for LazyColumn performance, and most beginner tutorials skip it entirely.

Here’s the problem without key:

Kotlin
// ❌ Without key — Compose can't track items
items(courses) { course ->
    CourseItem(course = course)
}
Kotlin

When your list changes — an item gets moved, deleted, or inserted — Compose has no way to know which items are which. It treats every position change as a deletion and creation. So if one item moves from position 3 to position 1, Compose recomposes positions 1, 2, and 3 — even if items 2 and 3 didn’t change at all.

Kotlin
// ✅ With key — Compose tracks each item by its stable ID
items(
    items = courses,
    key = { course -> course.id }
) { course ->
    CourseItem(course = course)
}
Kotlin

With a stable key, Compose knows exactly which item is which. If item with id=3 moves to position 1, Compose moves it rather than recomposing everything. According to the official Android performance best practices, providing stable keys for each item lets Compose avoid unnecessary recompositions — which directly translates to smoother scrolling and lower battery usage.

The key must be:

  • Unique for each item
  • Stable across recompositions — a database ID, a UUID, or any value that doesn’t change as the list updates
  • Never the item’s position in the list — positions change, IDs don’t

Adding a List Header

Real apps almost always need a header above the list — a title, a filter row, a search bar. LazyColumn handles this cleanly with item {}:

Kotlin
LazyColumn(
    modifier = Modifier.fillMaxSize(),
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(12.dp)
) {
    // Single header item
    item {
        Text(
            text = "KtDevLog Courses",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(bottom = 8.dp)
        )
    }

    // The list items
    items(
        items = courses,
        key = { it.id }
    ) { course ->
        CourseItem(course = course, onClick = {})
    }

    // Footer item
    item {
        Text(
            text = "${courses.size} courses available",
            fontSize = 13.sp,
            color = Color.Gray,
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 8.dp),
            textAlign = TextAlign.Center
        )
    }
}
Kotlin

item {} inserts a single composable into the lazy list at that position — perfectly scrollable alongside the rest of the list. Mix item {} and items() as many times as needed.

Sticky Headers — Group Your List

Sticky headers stay pinned at the top of the screen as the user scrolls through each group. In 2026, this is still an experimental API but widely used in production:

Kotlin
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GroupedCourseList(groupedCourses: Map<String, List<Course>>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp)
    ) {
        groupedCourses.forEach { (category, courses) ->

            // Sticky header — stays pinned while scrolling through group
            stickyHeader {
                Surface(
                    modifier = Modifier.fillMaxWidth(),
                    color = Color(0xFFF1F5F9)
                ) {
                    Text(
                        text = category,
                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Bold,
                        color = Color(0xFF7C3AED)
                    )
                }
            }

            // Items in this group
            items(
                items = courses,
                key = { it.id }
            ) { course ->
                CourseItem(course = course, onClick = {})
            }
        }
    }
}
Kotlin
Kotlin
// Usage
val groupedCourses = mapOf(
    "Kotlin Fundamentals" to listOf(course1, course2, course3),
    "Jetpack Compose" to listOf(course4, course5),
    "Architecture" to listOf(course6, course7, course8)
)
Kotlin

Scroll to Top Button — Using rememberLazyListState

Here’s a pattern you’ll use in almost every real list screen — a “scroll to top” button that appears only after the user has scrolled down:

Kotlin
@Composable
fun CourseListWithScrollButton(courses: List<Course>) {
    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()

    // derivedStateOf — only recomposes when showButton actually changes
    val showScrollButton by remember {
        derivedStateOf { listState.firstVisibleItemIndex > 0 }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn(
            state = listState,
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(16.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            items(
                items = courses,
                key = { it.id }
            ) { course ->
                CourseItem(course = course, onClick = {})
            }
        }

        // Scroll to top button — animated visibility
        AnimatedVisibility(
            visible = showScrollButton,
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp)
        ) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch {
                        listState.animateScrollToItem(0)
                    }
                },
                containerColor = Color(0xFF7C3AED)
            ) {
                Icon(
                    imageVector = Icons.Default.KeyboardArrowUp,
                    contentDescription = "Scroll to top",
                    tint = Color.White
                )
            }
        }
    }
}
Kotlin

The derivedStateOf wrapper here is critical — and it’s one of the performance tips that came directly from the official 2026 Android documentation. Without derivedStateOf, showScrollButton would trigger recomposition on every single scroll event as firstVisibleItemIndex changes. With derivedStateOf, recomposition only happens when showScrollButton actually changes from false to true or back — which is exactly what you want.

This pattern works beautifully alongside the reactive state management patterns from Kotlin StateFlow when your list is driven by a ViewModel.

Performance Rules — What Not to Do

Here’s the performance section most LazyColumn tutorials gloss over. These mistakes will silently slow your list even when your item count is small.

❌ Don’t Sort Inside LazyColumn

Kotlin
// ❌ Wrong — sorts every time the list recomposes
LazyColumn {
    items(courses.sortedBy { it.title }) { course ->
        CourseItem(course)
    }
}
Kotlin
Kotlin
// ✅ Correct — sort once, remember the result
val sortedCourses = remember(courses) {
    courses.sortedBy { it.title }
}
LazyColumn {
    items(
        items = sortedCourses,
        key = { it.id }
    ) { course ->
        CourseItem(course)
    }
}
Kotlin

❌ Don’t Use Non-Stable Keys

Kotlin
// ❌ Wrong — position changes as list updates
items(courses, key = { index -> index }) { ... }

// ✅ Correct — stable unique ID
items(courses, key = { course -> course.id }) { ... }
Kotlin

❌ Don’t Build Heavy Composables Inline

Kotlin
// ❌ Wrong — complex logic inside the items block causes recomposition thrash
items(courses) { course ->
    val processedData = expensiveOperation(course) // Runs on every scroll
    CourseItem(processedData)
}
Kotlin
Kotlin
// ✅ Correct — extract into a separate composable
items(courses, key = { it.id }) { course ->
    CourseItem(course = course, onClick = {})
}

// CourseItem handles its own internal logic cleanly
Kotlin

LazyColumn vs RecyclerView — The Real Comparison

If you’re coming from XML-based Android development, here’s the honest side-by-side:

RecyclerView (XML)LazyColumn (Compose)
Adapter needed✅ Yes❌ No
ViewHolder needed✅ Yes❌ No
Item layout XML✅ Yes❌ No
DiffUtil needed✅ Yes❌ No (keys handle this)
Click handlingCallback patternLambda directly in composable
Item animationsManual animator setupAnimatedVisibility, animateItemPlacement
Lines of boilerplate~80–150 lines~15–30 lines
PerformanceExcellentExcellent

The performance is comparable. The development speed and maintainability favour LazyColumn significantly. Every new Android project in 2026 should default to LazyColumn unless there’s a specific reason to use RecyclerView.

For building the navigation between your list and detail screens, Jetpack Compose Navigation shows exactly how to pass the selected item’s ID to a detail screen — the standard pattern for list-detail apps.

Frequently Asked Questions

What is LazyColumn in Jetpack Compose?

LazyColumn is a vertically scrolling list composable in Jetpack Compose that only composes and lays out items currently visible on screen. It’s the Compose equivalent of RecyclerView — but without the need for an Adapter, ViewHolder, or XML item layout. You simply pass your list to items() inside the LazyColumn block, and Compose handles all the rendering and recycling automatically.

What is the difference between Column and LazyColumn in Jetpack Compose?

Column renders all its children immediately and doesn’t scroll by default — it’s suitable for a small, fixed number of items. LazyColumn only renders visible items, handles scrolling automatically, and is optimised for large or dynamic lists. Use Column for 2 to 5 static items. Use LazyColumn for anything that might grow, anything loaded from a database or API, or anything the user might scroll through.

Why should I use item keys in LazyColumn?

Item keys help Compose track individual items across list changes. Without keys, any list update causes Compose to recompose every affected position — even items that didn’t change. With a stable unique key like a database ID, Compose can identify exactly which items moved, were added, or were removed — and only recompose what actually changed. This directly improves scroll performance and prevents unnecessary animations.

How do I add images to LazyColumn items in Jetpack Compose?

Use the Coil library with AsyncImage for loading remote images, or Image with a local drawable resource for static images. Add Coil to your build.gradle.kts with implementation("io.coil-kt:coil-compose:2.7.0"), then use AsyncImage(model = imageUrl, contentDescription = "...") inside your item composable. Coil handles async loading, disk caching, and error states automatically.

Is LazyColumn better than RecyclerView?

For new projects using Jetpack Compose, yes. LazyColumn achieves comparable scrolling performance to RecyclerView but with dramatically less boilerplate — no Adapter, ViewHolder, DiffUtil, or XML item layouts required. It integrates naturally with Compose state management, click handlers are simple lambdas, and animations work with AnimatedVisibility and animateItemPlacement. For existing XML-based apps, RecyclerView remains perfectly valid — migration should be gradual and purposeful.

Conclusion

This Jetpack Compose LazyColumn example guide covered everything from a simple string list to a fully featured card list with images, text, headers, sticky sections, and a scroll-to-top button. You’ve seen why key is non-negotiable for performance, how derivedStateOf prevents unnecessary recomposition, and why LazyColumn replaces RecyclerView without any of the boilerplate.

The pattern is always the same: define your data model, build your item composable, pass your list to LazyColumn with stable keys, and let Compose handle the rest. No Adapter. No ViewHolder. No XML.

Start with the CourseList example from this guide. Drop in your own data class. Style the CourseItem to match your app’s design. Add the scroll-to-top button when your list gets long. Once you’ve built one LazyColumn that feels smooth and professional, you’ll build every future list in Compose without a second thought.

For the full modern Android architecture picture — connect your LazyColumn to a ViewModel using Kotlin StateFlow, model your loading and error states with Kotlin sealed classes, and wire up navigation to a detail screen with Jetpack Compose Navigation. Those three pieces together give you a complete, production-quality list feature.

RecyclerView had a great run. LazyColumn just made it optional.

Tags: jetpack compose lazycolumn example
SharePinTweet
Md Sharif Mia

Md Sharif Mia

Md Sharif Mia is a Kotlin and Android developer with hands-on experience building real-world Android applications using Kotlin, Jetpack Compose, and Firebase. He created KtDevLog to help aspiring Android developers learn through practical, step-by-step tutorials — from writing their first line of Kotlin to shipping complete apps. Through KtDevLog, Sharif shares what actually works in Android development: clean code patterns, common beginner mistakes to avoid, and project-based lessons that go beyond theory. His writing style is direct and beginner-friendly, making complex Android concepts easy to understand for developers at any stage. When he is not writing tutorials, Sharif is experimenting with new Android features, exploring Kotlin best practices, and building apps that solve everyday problems.

Related Posts

Remember vs rememberSaveable in Jetpack Compose Explained
Jetpack Compose

Remember vs rememberSaveable in Jetpack Compose Explained

May 2, 2026

You've built a form. The user carefully types their email address, their name, their...

Jetpack Compose Navigation Tutorial: Pass Arguments Easily
Jetpack Compose

Jetpack Compose Navigation Tutorial: Pass Arguments Easily

April 29, 2026

Think about the last app you used on your phone. You tapped a product...

Row in Jetpack Compose: 5 Essential Layout Tips
Jetpack Compose

Row in Jetpack Compose: 5 Essential Layout Tips

April 23, 2026

Picture a restaurant menu. The dish name is on the left. The price is...

Your First Jetpack Compose Function Explained
Jetpack Compose

Your First Jetpack Compose Function Explained

April 21, 2026

If you've just created a new Android project in Android Studio and stared at...

Comments 1

  1. Pingback: Remember vs rememberSaveable in Jetpack Compose Explained

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • About Us
  • Contact Us
  • Privacy Policy
  • Terms & Conditions

© Copyright 2026 KtDevLog. All Rights Reserved.

Welcome Back!

Login to your account below

Forgotten Password?

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
  • Home
  • Jetpack Compose
  • Kotlin Fundamentals
  • Android Studio

© Copyright 2026 KtDevLog. All Rights Reserved.