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 inSavedStateHandleso it survives rotation). - Derive a flat
List<VisibleComment>where each entry carries itsdepth, walking the tree and stopping at nodes that are not expanded. - Render it in a single
LazyColumnwithkey = { it.id }andModifier.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) },
)
}
}
}