What You Will Build
A full chat screen with a conversation thread, message bubbles aligned left (received) and right (sent), timestamps, an online status indicator, and a text input with send button. Messages are added to the list in real time.
Why This Pattern Matters
Chat UIs are one of the most commonly built screens in Android development. This project teaches reversed LazyColumn for bottom-anchored scrolling, conditional alignment for sent/received messages, and stateful message lists.
Step 1: Message Model and State
data class Msg(
val text: String,
val isMe: Boolean,
val time: String
)
var messages by remember {
mutableStateOf(listOf(
Msg("Hey! How are you?", false, "10:30"),
Msg("I'm good, thanks! Working on the app.", true, "10:31"),
Msg("Sounds great! Can you share a screenshot?", false, "10:32"),
Msg("Sure, sending now!", true, "10:33")
))
}
var input by remember { mutableStateOf("") }
Step 2: Chat Bubbles with Conditional Alignment
LazyColumn(
modifier = Modifier.weight(1f).padding(horizontal = 16.dp),
reverseLayout = true
) {
items(messages.reversed()) { msg ->
Row(
Modifier.fillMaxWidth().padding(vertical = 4.dp),
horizontalArrangement =
if (msg.isMe) Arrangement.End
else Arrangement.Start
) {
Card(
colors = CardDefaults.cardColors(
containerColor =
if (msg.isMe)
MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant
),
shape = RoundedCornerShape(16.dp)
) {
Column(Modifier.padding(12.dp)) {
Text(msg.text,
color = if (msg.isMe) Color.White
else MaterialTheme.colorScheme
.onSurface)
Text(msg.time, fontSize = 10.sp,
color = if (msg.isMe)
Color.White.copy(alpha = 0.7f)
else MaterialTheme.colorScheme
.onSurfaceVariant,
modifier = Modifier.align(Alignment.End))
}
}
}
}
}
Step 3: Input Bar with Send
Row(
Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Message...") },
shape = RoundedCornerShape(24.dp),
singleLine = true
)
Spacer(Modifier.width(8.dp))
FilledIconButton(onClick = {
if (input.isNotBlank()) {
messages = messages + Msg(input, true, "Now")
input = ""
}
}) {
Icon(Icons.Default.Send, "Send")
}
}
Tips and Pitfalls
- reverseLayout = true makes LazyColumn start from the bottom, which is essential for chat. New messages appear at the bottom naturally.
- messages.reversed() is needed because reverseLayout reverses display order — without reversing the data, newest messages appear at top.
- RoundedCornerShape(24.dp) on the TextField creates a pill-shaped input common in chat apps.
- Real app extension: Add
rememberLazyListState()and scroll to bottom on new message withanimateScrollToItem(0).