Jetpack Compose

ViewModel and rememberSaveable

The Truth About ViewModel and rememberSavable: Configuration Changes vs Process Death

If you’ve built Android apps with Jetpack Compose, you’ve probably run into the question: Should I use ViewModel or rememberSaveable? Both help you keep state alive, but they work very differently depending on what’s happening to your app — like when the screen rotates or when the system kills your process.

This post will break down ViewModel and rememberSaveable, explain when to use each, and show real code examples so it finally clicks.

The Basics: Why State Preservation Matters

On Android, your app doesn’t always stay alive. Two big events affect your app’s state:

  1. Configuration changes — like screen rotations, language changes, or switching dark mode. The activity is destroyed and recreated, but the process usually stays alive.
  2. Process death — when Android kills your app’s process (e.g., to reclaim memory) and later restores it when the user comes back.

If you don’t handle these correctly, your users lose whatever they were doing. That’s where ViewModel and rememberSaveable come in.

remember: The Starting Point in Compose

At the simplest level, you use remember in Jetpack Compose to keep state alive across recompositions.

Kotlin
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}
  • Here, count won’t reset when Compose redraws the UI.
  • But if the device rotates (configuration change), the state is lost because remember only survives recompositions, not activity recreation.

That’s why we need more powerful tools.

rememberSaveable: Survives Configuration Changes and Process Death

rememberSaveable goes one step further. It automatically saves your state into a Bundle using Android’s saved instance state mechanism.

Kotlin
@Composable
fun CounterScreen() {
    var count by rememberSaveable { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

What happens here:

  • Rotate the screen? count survives.
  • App is killed and restored (process death)? count also survives, because it was written to the saved instance state.

Limitations:

  • Only works with data types that can be written to a Bundle (primitives, Strings, parcelables, etc.).
  • Not ideal for large objects or data fetched from a repository.

ViewModel: Survives Configuration Changes, Not Process Death

A ViewModel is a lifecycle-aware container designed to hold UI data. It’s tied to a LifecycleOwner like an activity or a navigation back stack entry.

Kotlin
class CounterViewModel : ViewModel() {
    var count by mutableStateOf(0)
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    Button(onClick = { viewModel.count++ }) {
        Text("Count: ${viewModel.count}")
    }
}

What happens here:

  • Rotate the screen? count survives. The same ViewModel instance is reused.
  • App is killed (process death)? count is lost. The ViewModel does not persist beyond process death.

Configuration Changes vs Process Death: Who Wins?

Here’s the clear breakdown:

When to Use rememberSaveable

Use rememberSaveable for small, lightweight UI state that:

  • Must survive both rotation and process death.
  • Can easily be serialized into a Bundle.

Examples:

  • Current tab index.
  • Form text fields.
  • Simple filter/sort options.

When to Use ViewModel

Use ViewModel for more complex or long-lived state that:

  • Doesn’t need to survive process death.
  • Might involve business logic, repositories, or data streams.
  • Should be scoped to the screen or navigation graph.

Examples:

  • Data loaded from a database or network.
  • Complex business logic.
  • State shared across multiple composables in the same screen.

Can You Combine Them? Yes.

Often, the best solution is to use ViewModel and rememberSaveable together.
 For example, a ViewModel manages your main UI state, but a few critical fields use rememberSaveable so they’re restored even after process death.

Kotlin
@Composable
fun FormScreen(viewModel: FormViewModel = viewModel()) {
    var userInput by rememberSaveable { mutableStateOf("") }

    Column {
        TextField(
            value = userInput,
            onValueChange = { userInput = it }
        )

        Button(onClick = { viewModel.submit(userInput) }) {
            Text("Submit")
        }
    }
}

Here:

  • userInput is lightweight and saved with rememberSaveable.
  • The ViewModel takes care of processing and persisting the submitted data.

Conclusion

The truth about ViewModel and rememberSaveable is simple once you think in terms of configuration changes vs process death:

  • remember → Only survives recomposition.
  • rememberSaveable → Survives both rotation and process death (small, serializable state).
  • ViewModel → Survives rotation, great for business logic, but not process death.

Use them in combination, not competition. Each tool has its place, and knowing when to reach for which makes your Compose apps more resilient, smoother, and user-friendly.

CompositionLocal

CompositionLocal Deep Dive: Writing Scalable UI in Jetpack Compose

When building Android apps with Jetpack Compose, you’ll often need to share data across your UI tree. Passing parameters down every Composable quickly becomes messy. That’s where CompositionLocal comes in.

Think of CompositionLocal as a smart way to provide values (like theme, locale, or user preferences) to multiple Composables without having to manually thread them through function parameters. It’s like dependency injection — but scoped to the Compose world.

In this post, we’ll explore how CompositionLocal works, why it matters for building scalable UI, and how you can use it effectively.

What is CompositionLocal?

CompositionLocal is a mechanism that allows you to define and access values that are automatically propagated down the Composable hierarchy.

  • It provides contextual values (like theme colors or configurations).
  • It removes the need to pass arguments everywhere.
  • It helps you scale UI architecture by keeping components decoupled.

Jetpack Compose already uses CompositionLocal under the hood for things like MaterialTheme, text styles, and layout direction.

Defining a CompositionLocal

You start by creating a CompositionLocal with a default value:

Kotlin
val LocalUser = compositionLocalOf<String> { 
    error("No user provided") 
}

Here:

  • compositionLocalOf creates a CompositionLocal with a default (or error if missing).
  • We’re saying: “If no user is provided, throw an error.”

Providing a Value

To inject a value, you use CompositionLocalProvider:

Kotlin
@Composable
fun AppContent() {
    CompositionLocalProvider(LocalUser provides "amol pawar") {
        UserProfile()
    }
}

Inside AppContent, any child Composable can access LocalUser.

Consuming a CompositionLocal

To read the value, use .current:

Kotlin
@Composable
fun Dashboard() {
    Column {
        CompositionLocalProvider(LocalUser provides "akshay") {
            UserProfile() // shows "Hello, askhay!"
        }
        UserProfile() // shows "Hello, amol pawar!"
    }
}

Output:

Hello, amol pawar!

No need to pass user down as a parameter—CompositionLocal handles it.

Why Use CompositionLocal?

Let’s break it down with a practical example. Imagine a large app with:

  • Theme data (colors, typography).
  • User session info.
  • App settings like dark mode, locale, etc.

Passing these manually would be a nightmare. With CompositionLocal, you define them once and let the UI tree consume them where needed.

Scoped Values for Flexibility

One powerful feature is scoping. You can override a CompositionLocal in a subtree without affecting the rest of the app.

Kotlin
@Composable
fun Dashboard() {
    Column {
        CompositionLocalProvider(LocalUser provides "akshay") {
            UserProfile() // shows "Hello, askhay!"
        }
        UserProfile() // shows "Hello, amol pawar!"
    }
}

The value depends on where the Composable is in the hierarchy. This makes it perfect for context-specific overrides (like previewing different themes).

Best Practices for CompositionLocal

  1. Don’t abuse it. Use CompositionLocal for global or contextual data, not just to avoid passing parameters.
  2. Keep defaults meaningful. Provide safe defaults or throw an error if the value is critical.
  3. Use for ambient context. Theme, locale, user, system settings — these are ideal use cases.
  4. Avoid hidden dependencies. If a Composable always needs a value, prefer explicit parameters for clarity.

Theme System with CompositionLocal

Let’s create a mini theme system:

Kotlin
data class MyColors(val primary: Color, val background: Color)

val LocalColors = staticCompositionLocalOf<MyColors> {
    error("No colors provided")
}

@Composable
fun MyTheme(content: @Composable () -> Unit) {
    val colors = MyColors(primary = Color.Blue, background = Color.White)
    CompositionLocalProvider(LocalColors provides colors) {
        content()
    }
}

@Composable
fun ThemedButton() {
    val colors = LocalColors.current
    Button(onClick = {}) {
        Text("Click Me", color = colors.primary)
    }
}

Usage:

Kotlin
@Composable
fun App() {
    MyTheme {
        ThemedButton()
    }

Here, ThemedButton gets its styling from LocalColors without needing parameters.

CompositionLocal vs Parameters

  • Use parameters when data is essential to the Composable.
  • Use CompositionLocal when data is contextual, like theming or configuration.

This balance keeps your UI scalable and maintainable.

Conclusion

CompositionLocal is one of the most powerful tools in Jetpack Compose for writing scalable UI. It keeps your code cleaner, reduces boilerplate, and makes context handling a breeze.

By using CompositionLocal wisely, you can:

  • Share contextual data easily
  • Override values locally
  • Keep UI components decoupled and reusable

Next time you’re passing a value through five different Composables, stop and ask yourself — could CompositionLocal handle this better?

Scrollable Sticky Table/Grid UIs in Jetpack Compose

Build Scrollable Sticky Table/Grid UIs in Jetpack Compose Like a Pro

If you’ve ever built dashboards, spreadsheets, or financial apps, you know how important tables are. But a plain table isn’t enough — you often need a Scrollable Sticky Table/Grid where headers stay in place while data scrolls smoothly. In the past, building this in Android XML layouts was painful. With Jetpack Compose, you can achieve it with...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Jetpack Compose LazyColumn Sticky Header

Jetpack Compose LazyColumn Sticky Header: Complete Implementation Guide

When you’re building long lists in Jetpack Compose, sometimes you need certain sections to stand out and stay visible while scrolling. That’s exactly where Sticky Header comes in. Imagine scrolling through a contacts app — the alphabet letter headers (A, B, C…) stick at the top while you browse through names. Jetpack Compose makes this easy with LazyColumn and stickyHeader.

In this guide, I’ll walk you through how to implement Sticky Header in Jetpack Compose with clear explanations.

What is a Sticky Header?

A Sticky Header is a UI element that “sticks” at the top of a scrollable list until the next header pushes it off. It’s commonly used in:

  • Contact lists
  • Calendar apps
  • Shopping category lists
  • News feeds with date separators

This improves navigation and makes large lists easier to scan.

Why Use Sticky Header in Jetpack Compose?

With Jetpack Compose, you don’t need RecyclerView adapters or complex custom views. The LazyColumn component handles large, scrollable lists efficiently, and stickyHeader makes adding sticky sections straightforward.

Benefits:

  • Simple syntax, no XML layouts.
  • Clean and declarative code.
  • Works seamlessly with Compose state management.

LazyColumn and stickyHeader Basics

Here’s the basic structure of a LazyColumn with a Sticky Header:

Kotlin
@Composable
fun StickyHeaderExample() {
    val sections = listOf(
        "Fruits" to listOf("Apple", "Banana", "Orange"),
        "Vegetables" to listOf("Carrot", "Potato", "Tomato"),
        "Dairy" to listOf("Milk", "Cheese", "Yogurt")
    )

    LazyColumn {
        sections.forEach { (header, items) ->
            stickyHeader {
                Text(
                    text = header,
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.LightGray)
                        .padding(8.dp),
                    style = MaterialTheme.typography.subtitle1
                )
            }

            items(items) { item ->
                Text(
                    text = item,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
                )
            }
        }
    }
}

Let’s break it down:

Data Setup

Kotlin
val sections = listOf(
    "Fruits" to listOf("Apple", "Banana", "Orange"),
    "Vegetables" to listOf("Carrot", "Potato", "Tomato"),
    "Dairy" to listOf("Milk", "Cheese", "Yogurt")
)

Here, each section has a header (like “Fruits”) and a list of items.

LazyColumn

Kotlin
LazyColumn { ... }

Displays the entire list efficiently. Only visible items are composed, so it’s memory-friendly.

stickyHeader

Kotlin
stickyHeader {
    Text(
        text = header,
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.LightGray)
            .padding(8.dp)
    )
}

This is the star of the show. The header stays pinned at the top while scrolling through its section.

items()

Kotlin
items(items) { item -> ... }

Renders each element under the sticky header.

Customizing Sticky Headers

You can style sticky headers to fit your app’s design. For example:

  • Add icons to headers.
  • Change background color based on section.
  • Apply elevation or shadows for better separation.

Example with custom styling:

Kotlin
stickyHeader {
    Surface(
        color = Color.DarkGray,
        shadowElevation = 4.dp
    ) {
        Text(
            text = header,
            modifier = Modifier
                .fillMaxWidth()
                .padding(12.dp),
            color = Color.White,
            style = MaterialTheme.typography.h6
        )
    }
}

When to Use and When Not To

Use Sticky Header when:

  • The list is grouped (categories, dates, sections).
  • Users need quick context while scrolling.

Avoid Sticky Header if:

  • The list is flat (no categories).
  • Too many headers clutter the UI.

Performance Considerations

LazyColumn with stickyHeader is optimized, but keep these in mind:

  • Keep headers lightweight (avoid heavy Composables inside).
  • Reuse stateful items outside of the list when possible.
  • Test on lower-end devices if you have very large datasets.

Conclusion

The Sticky Header in Jetpack Compose makes complex, sectioned lists much easier to build and navigate. With just a few lines of code inside a LazyColumn, you can create polished, user-friendly experiences without dealing with RecyclerView boilerplate.

If you’re building apps with grouped data — contacts, shopping categories, or event timelines — Sticky Header is a feature you’ll definitely want to use.

Sticky Header in Jetpack Compose

How to Create a Sticky Header in Jetpack Compose

If you’ve ever scrolled through a long list in an app and noticed that the section title stays pinned at the top until the next section appears, you’ve seen a sticky header. Sticky headers make lists easier to navigate, especially when content is grouped by category.

In this post, we’ll learn step by step how to implement a Sticky Header in Jetpack Compose using LazyColumn. Don’t worry if you’re just getting started with Compose—the explanation will stay simple, with code examples and clear breakdowns.

What is a Sticky Header?

A sticky header is a UI element that remains visible at the top of a scrolling list while the content beneath it scrolls. It’s often used in apps like Contacts (where the alphabet letters stick as you scroll) or e-commerce apps (where categories like “Shoes,” “Bags,” or “Clothing” stay pinned).

Jetpack Compose makes this much easier to implement compared to the old RecyclerView approach in XML.

Why Use Sticky Headers in Jetpack Compose?

Adding a Sticky Header in Jetpack Compose improves:

  • Readability: Users instantly know which section they’re in.
  • Navigation: Helps users scan through grouped content quickly.
  • User Experience: Feels modern, polished, and professional.

The Key Composable: stickyHeader

Jetpack Compose provides a built-in modifier inside LazyColumn called stickyHeader. This allows you to define a composable item that “sticks” to the top while scrolling.

Basic Code Example

Here’s a simple example of creating a Sticky Header in Jetpack Compose:

Kotlin
@Composable
fun StickyHeaderList() {
    val groupedItems = mapOf(
        "Fruits" to listOf("Apple", "Banana", "Mango", "Orange"),
        "Vegetables" to listOf("Carrot", "Potato", "Tomato"),
        "Drinks" to listOf("Water", "Juice", "Soda")
    )

    LazyColumn {
        groupedItems.forEach { (header, items) ->
            stickyHeader {
                Text(
                    text = header,
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.LightGray)
                        .padding(16.dp),
                    fontWeight = FontWeight.Bold
                )
            }

            items(items) { item ->
                Text(
                    text = item,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(12.dp)
                )
            }
        }
    }
}

Let’s break it down so it’s crystal clear:

Grouped Data

  • We created a Map with categories as keys ("Fruits", "Vegetables", "Drinks") and a list of items under each.

LazyColumn

  • Works like a RecyclerView but in Compose. It’s efficient for large lists.

stickyHeader

  • This is the magic. Whatever you put inside stickyHeader will remain stuck at the top until another header replaces it.
  • We used a Text with background color and padding so it looks like a section header.

items()

  • Displays each element in the list under its header.

Styling the Sticky Header

You don’t want your sticky header to look boring. Here are a few tweaks you can add:

Kotlin
stickyHeader {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = Color.DarkGray,
        shadowElevation = 4.dp
    ) {
        Text(
            text = header,
            modifier = Modifier.padding(16.dp),
            color = Color.White,
            fontSize = 18.sp,
            fontWeight = FontWeight.Bold
        )
    }
}

This adds:

  • Background color (DarkGray)
  • Shadow elevation for depth
  • White text for contrast

When to Use Sticky Headers

Sticky headers are perfect for:

  • Contact lists grouped alphabetically
  • Shopping apps with categories
  • News apps with sections (e.g., Sports, Tech, Business)
  • Music playlists grouped by artist or album

Common Mistakes to Avoid

  • Too many sticky headers: Don’t overuse them — it can feel cluttered.
  • No visual distinction: Make sure headers look different from list items.
  • Performance issues: For extremely large datasets, consider lazy loading.

Conclusion

Creating a Sticky Header in Jetpack Compose is simple, thanks to the stickyHeader API inside LazyColumn. With just a few lines of code, you can build a smooth, user-friendly list that looks polished and professional.

As Compose continues to evolve, features like these make UI development faster, cleaner, and more intuitive. Whether you’re building a contacts app, a shopping app, or just experimenting, sticky headers will give your lists a better structure and improve the user experience.

Pro Tip: Always test on different screen sizes to make sure your headers remain clear and readable.

Now it’s your turn — try adding a sticky header to your own Jetpack Compose project and see the difference!

State Hoisting in Jetpack Compose

State Hoisting in Jetpack Compose: Best Practices for Scalable Apps

When building Android apps with Jetpack Compose, state management is one of the most important pieces to get right. If you don’t handle state properly, your UI can become messy, tightly coupled, and hard to scale. That’s where State Hoisting in Jetpack Compose comes in. In this post, we’ll break down what state hoisting is,...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
State Management in Jetpack Compose

Mastering State Management in Jetpack Compose: A Comprehensive Guide

State management is one of the most critical aspects of building dynamic and interactive Android applications. With Jetpack Compose, Android’s modern UI toolkit, managing state becomes more intuitive, but it also introduces new paradigms that developers need to understand. In this blog, we’ll explore state management in Jetpack Compose in detail. We’ll break down essential...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
collectAsStateWithLifecycle

Lifecycle-Aware State in Compose: Why collectAsStateWithLifecycle Outperforms collectAsState

Jetpack Compose makes UI state management feel almost magical — you observe a Flow, call collectAsState(), and your composable stays up to date. But here’s the catch: not all flows are equal when it comes to lifecycle awareness. If you’re building Android apps today, you should almost always be reaching for collectAsStateWithLifecycle instead of the collectAsState. Let’s break...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Why Side-Effect APIs Matter in Jetpack Compose

Why Side-Effect APIs Matter in Jetpack Compose — And How to Use Them the Right Way

Jetpack Compose has completely changed how we build Android UIs. With its declarative approach, you just describe what your UI should look like, and Compose takes care of the rest. But here’s the thing: your app isn’t only about drawing screens. There are things like showing a toast, requesting a permission, or launching a background...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Demystifying SideEffect, LaunchedEffect & DisposableEffect in Jetpack Compose

Demystifying SideEffect, LaunchedEffect & DisposableEffect in Jetpack Compose

Jetpack Compose, Android’s modern UI toolkit, introduces a declarative approach to building user interfaces. With this shift comes a new way of thinking about side effects — operations that interact with the outside world or perform actions outside the scope of a composable function. Understanding how to manage these side effects properly is crucial to building reliable,...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
error: Content is protected !!