← Coding round practice

Expandable Nested List (Comments with Replies)

MediumLists · RecursionO(n) flatten per expand

The problem

Render a comment thread where each comment can have replies, and replies can have replies.

Requirements:

  • Tap a comment to expand or collapse its replies, with the indentation showing depth.
  • Expanded state must survive scrolling away and back, and rotation.
  • Smooth scroll with hundreds of comments.
Stuck? Open one hint

A LazyColumn wants a flat list. Can you keep the tree as data and flatten only what is visible?

Show the worked solution

The naive approach nests a Column inside each item and recurses. It works for a demo and falls over at scale — nothing is lazy any more and the whole tree composes at once.

The answer they want: keep the tree in the data layer, flatten it for display.

  • Hold expandedIds: Set<String> in the ViewModel (and in SavedStateHandle so it survives rotation).
  • Derive a flat List<VisibleComment> where each entry carries its depth, walking the tree and stopping at nodes that are not expanded.
  • Render it in a single LazyColumn with key = { it.id } and Modifier.padding(start = depth * 16.dp).

Now only visible rows compose, keys keep the expanded state on the right row, and depth is just padding. Mention capping depth visually — at depth 8 the text column is unreadable, so real apps flatten after a few levels.

KotlinO(n) flatten per expand
data class Comment(val id: String, val text: String, val replies: List<Comment> = emptyList())
data class VisibleComment(val comment: Comment, val depth: Int, val isExpanded: Boolean)

fun flatten(
    comments: List<Comment>,
    expanded: Set<String>,
    depth: Int = 0,
    out: MutableList<VisibleComment> = mutableListOf(),
): List<VisibleComment> {
    for (c in comments) {
        val isExpanded = c.id in expanded
        out += VisibleComment(c, depth, isExpanded)
        if (isExpanded) flatten(c.replies, expanded, depth + 1, out)
    }
    return out
}

@Composable
fun Thread(rows: List<VisibleComment>, onToggle: (String) -> Unit) {
    LazyColumn {
        items(rows, key = { it.comment.id }) { row ->
            CommentRow(
                comment = row.comment,
                modifier = Modifier.padding(start = (row.depth * 16).dp),
                onClick = { onToggle(row.comment.id) },
            )
        }
    }
}