Implementing an animated column section header using Jetpack Compose
Tags: android, jetpackcompose, ui, kotlin
Published: 2023-11-30 17:33:57

A while ago, I was asked how I would build a temporary animated column section header in Jetpack Compose. Now, you may be thinking *What on earth is that?* Here's a short clip that shows the result of my efforts.

{% youtube byRTitk737M %}

This vertically scrolling list contains ordinary items (*Item #1*, *Item #2*, *Item #3*, …) and section headers (*Header #1*, *Header #2*, *Header #2*, …). When the section of the first visible item changes, a copy of the section header is made visible, and, after a few seconds, becomes invisible again. How the animation takes place (fading in / out, flying in / out, …), can be changed easily with Jetpack Compose. Styling the elements (fonts, colors, sizes, …) is no challenge at all, too. Therefore, let's focus on

- adding section headers
- animating a copy of the section header for the first visible item

The source code is available as a [GitHub Gist](https://gist.github.com/tkuenneth/9789dfc63ea352491fb52cd0ad7a5622). Allow me to walk you through it step by step.

```kotlin
class MainActivity : ComponentActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MaterialTheme {
        val list = mutableListOf<ListItem>()
        (1..10).forEach { headerIndex ->
          (1..(3..10).random()).forEach { itemIndex ->
            list.add(
              ListItem(
                header = "Header #$headerIndex",
                text = "Item #$itemIndex"
              )
            )
          }
        }
        LazyColumnDemo(list)
      }
    }
  }
}
```

The list consists of ten sections. Each section consists of a random number of items, at least three, but not more than ten. Each item is represented by a `ListItem`.

```kotlin
data class ListItem(
  val header: String, 
  val text: String
)
```

As you will see shortly, this class makes it easy to determine the visibility of the temporary section header. However, it repeats the header name for items belonging to the same section, which could be considered waste of memory. But if you have no plans of showing thousands of items, I don't think this is going to be a problem.

Next, let's look at the `LazyColumnDemo()` composable.

```kotlin
@Composable
fun LazyColumnDemo(items: List<ListItem>) {
  Box(
    modifier = Modifier
      .fillMaxSize()
      .background(color = MaterialTheme.colorScheme.background)
  ) {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) {
      itemsIndexed(items = items) { index, item ->
        if ((index == 0) ||
            items[index - 1].header != item.header) {
          Header(text = item.header)
        }
        Item(text = item.text)
      }
    }
    val currentHeader by remember {
      derivedStateOf {
        items[listState.firstVisibleItemIndex].header
      }
    }
    var lastHeader by remember { mutableStateOf(currentHeader) }
    val headerVisible by remember(
      currentHeader, lastHeader
    ) { mutableStateOf(lastHeader != currentHeader) }
    AnimatedVisibility(visible = headerVisible) {
      Header(text = currentHeader)
      LaunchedEffect(key1 = currentHeader) {
        delay(3000)
        lastHeader = currentHeader
      }
    }
  }
}
```

As you can see, the parent composable, a `Box()`, has two children: `LazyColumn()` and `AnimatedVisibility`. We want to place the temporary column section header over the list, so `Box()` is a natural choice, because it stacks its content. You could even use the `contentAligmnent` parameter to position the header. `AnimatedVisibility()` has two children, too: `Header()`, which is the column section header, and `LaunchedEffect()`, which toggles the visibility. Let's see how this works.

When `LaunchedEffect()` enters the composition it will launch the provided block into the composition's `CoroutineContext`. The coroutine will be cancelled and re-launched when `LaunchedEffect()` is recomposed with a different `key1` (`currentHeader`). What does that mean? When the value of `currentHeader` changes, `LaunchedEffect` waits three seconds and then assigns the value of `currentHeader` to `lastHeader`. How does this change the visibility of the temporary header? `AnimatedVisibility()` receives a `visible` parameter with the value of `headerVisible`, which in turn is a state that changes based on two other states, `currentHeader` and `lastHeader`. So if any of them changes, `headerVisible` will be set to the result of the computation `lastHeader != currentHeader`. As after three seconds `lastHeader` is set to `currentHeader`, the temporary header will become invisible.

When does `currentHeader` change? It is a derived state of the expression `items[listState.firstVisibleItemIndex].header`: when the index of the first visible item changes, `currentHeader` will be automatically updated. `listState` is a remembered list state (`rememberLazyListState()`).

As we now have covered all states, let's dig into the list, in my case, a `LazyColumn()`. List items are created using `itemsIndexed`. Each item consists of one or two composables.

The first one, `Header()`, is shown when the very first item is displayed, or when the section header of the item to display differs from the section header of the previous item: `items[index - 1].header != item.header`. This code explains why I decided to put the header into my `ListItem` class: the comparison is beautifully simple.

```kotlin
@Composable
fun Header(text: String) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
      .padding(all = 16.dp),
    style = MaterialTheme.typography.headlineSmall,
    textAlign = TextAlign.Center
  )
}
```

The second one, `Item()`, is, well, an ordinary list item.

```kotlin
@Composable
fun Item(text: String) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
      .padding(all = 16.dp),
    style = MaterialTheme.typography.bodyLarge,
    textAlign = TextAlign.Center
  )
}
```

Obviously, `Header()` and `Item()` look pretty similar. The code in this article was deliberately kept simple for better understandability. Your implementation will likely style both differently.

### Conclusion

Jetpack Compose makes it really easy to show and hide UI elements based on the state of a list. Just remember the list state, create a derived sate from it, and pass it to one of the animation composables.

Have you created section headers for lists in Jetpack Compose? How did this work for you? Please share your thoughts in the comments.