'Sticky headers like android contacts in Jetpack Compose

I didn't find any way to have a stickyHeader on the same line as an item in a LazyColumn:

inline sticky headers

So I used a Box to put the letter with its background on top of the LazyColumn and used the LazyListState to put it in the right position :

@Preview
@Composable
fun InlineStickyList() {
    val unsorted = listOf(...) // Pair("FirstName","LastName")
    val data = unsorted.sortedBy { it.second[0] }
    val groups = data.groupBy { it.second[0] }

    @Composable
    fun Person(pair: Pair<String, String>, showLetter: Boolean) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.White),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                "${pair.second[0]}",
                modifier = Modifier
                    .padding(10.dp)
                    .alpha(if (showLetter) 1f else 0f),
                fontSize = 20.sp
            )
            Text("${pair.first} ${pair.second}")
        }
    }

    val state = rememberLazyListState()
    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn(modifier = Modifier.fillMaxSize(), state = state) {
            groups.forEach { (_, group) ->
                item {
                    Person(group[0], true)
                }
                items(group.subList(1, group.size)) {
                    Person(it, false)
                }
            }
        }
    }
    val letter =
        data[state.firstVisibleItemIndex].second[0]
    val next =
        data[state.firstVisibleItemIndex + 1].second[0]

    Text(
        "$letter",
        fontSize = 20.sp,
        modifier = Modifier
            .clipToBounds()
            .offset(y = if (letter != next) -with(LocalDensity.current) {
                state.firstVisibleItemScrollOffset.toDp()
            } else 0.dp)
            .background(Color.White)
            .padding(start = 10.dp, top = 10.dp, bottom = 10.dp),
    )
}

Is there another way to achieve that ? (cleaner/better)



Solution 1:[1]

I did a custom implementation... Not sure if it's the best solution, but here it is...

enter image description here

@Composable
fun LetterHeader(char: String, modifier: Modifier = Modifier) {
    Text(
        text = char,
        color = Color(117, 137, 199),
        fontSize = 28.sp,
        modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)
    )
}

There's nothing special in the function above. It represents a single char which acts as a "sticky header", but it will be also used in the item list as we can see below.

@Composable
fun NameItem(
    name: String,
    showCharHeader: Boolean,
    modifier: Modifier
) {
    Row(Modifier.fillMaxWidth()) {
        if (showCharHeader) {
            LetterHeader(
                char = name.first().toString(),
                modifier = modifier
            )
        } else {
            Spacer(modifier = modifier)
        }
        Text(
            text = name,
            color = Color.DarkGray,
            fontSize = 14.sp,
            modifier = Modifier.fillMaxWidth().padding(16.dp)
        )
    }
}

The function above represents each list item. Notice we're reusing the LetterHeader here. It will be displayed conditionally.

Finally, let's see the list...

@Composable
fun ListWithCustomStickHeaderScreen() {
    Box(Modifier.fillMaxSize()) {
        // List of names grouped by first char
        val groupedNames = remember(names) {
            names.groupBy { it.first() }
        }
        // Start indexes in the names list for each char
        val startIndexes = remember(names) {
            getStartIndexes(groupedNames.entries)
        }
        // End indexes in the names list for each char
        val endIndexes = remember(names) {
            getEndIndexes(groupedNames.entries)
        }
        // This commonModifier is used for both 
        // NameItem and LetterHeader
        val commonModifier = Modifier.width(50.dp)
        val listState = rememberLazyListState()
        // We're going to move the stick header up in case of the
        // first visible list index is one of last indexes
        val moveStickyHeader by remember {
            derivedStateOf {
                endIndexes.contains(listState.firstVisibleItemIndex + 1)
            }
        }
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            state = listState,
        ) {
            itemsIndexed(names) { index, name ->
                NameItem(
                    name,
                    // Showing the char header in the list item
                    // just in case it is one of the start indexes
                    // and it is not the first visible index
                    showCharHeader = startIndexes.contains(index) && listState.firstVisibleItemIndex != index,
                    commonModifier
                )
            }
        }
        LetterHeader(
            char = names[listState.firstVisibleItemIndex].first().toString(),
            modifier = commonModifier.then(
                // Moving up the sticky header using offset modifier.
                if (moveStickyHeader) {
                    Modifier.offset {
                        IntOffset(0, -listState.firstVisibleItemScrollOffset)
                    }
                } else {
                    Modifier
                }
            )
        )
    }
}

The full code is available here.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1