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 with animateScrollToItem(0).

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.