Building a chat user interface (UI) is a common requirement for many mobile applications. Jetpack Compose, Android’s modern UI toolkit, offers a declarative and efficient way to create such interfaces. This blog post will guide you through building a chat UI using Jetpack Compose, complete with message display, input fields, and styling.
Introduction to Chat UI with Jetpack Compose
A chat UI typically includes the following components:
- Message List: A scrollable list to display chat messages.
- Input Field: A text field where users can type their messages.
- Send Button: A button to send the typed message.
- Styling: Appropriate theming and styling for a polished look.
Why Use Jetpack Compose for Chat UI?
- Declarative: Compose’s declarative nature makes UI development more intuitive and easier to manage.
- Reusability: Components can be easily reused and customized.
- State Management: Seamless integration with state management solutions (like ViewModel).
- Animations: Built-in support for animations enhances user experience.
Steps to Build a Chat UI in Jetpack Compose
Step 1: Set Up Dependencies
Ensure you have the necessary dependencies in your build.gradle
file:
dependencies {
implementation("androidx.compose.ui:ui:1.6.4")
implementation("androidx.compose.material:material:1.6.4")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.4")
implementation("androidx.compose.runtime:runtime:1.6.4")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.compose.foundation:foundation:1.6.4")
// Optional: For Coil image loading
implementation("io.coil-kt:coil-compose:2.5.0")
}
Step 2: Create a Message Data Class
Define a data class to represent a chat message:
data class Message(
val text: String,
val isUserMessage: Boolean
)
Step 3: Implement the Chat Screen Composable
Create the main composable function for the chat screen. This includes the message list, input field, and send button.
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun ChatScreen() {
var messageText by remember { mutableStateOf(TextFieldValue("")) }
var messages by remember { mutableStateOf(listOf()) }
Column(modifier = Modifier.fillMaxSize()) {
// Message List
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(8.dp),
reverseLayout = true
) {
items(messages.reversed()) { message ->
ChatMessageItem(message = message)
}
}
// Input Field and Send Button
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = messageText,
onValueChange = { messageText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Type a message...") }
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(onClick = {
if (messageText.text.isNotBlank()) {
messages = messages + Message(messageText.text, true)
messageText = TextFieldValue("") // Clear input field
}
}) {
Icon(Icons.Filled.Send, contentDescription = "Send")
}
}
}
}
@Composable
fun ChatMessageItem(message: Message) {
val backgroundColor = if (message.isUserMessage) MaterialTheme.colors.primary else MaterialTheme.colors.surface
val alignment = if (message.isUserMessage) Alignment.End else Alignment.Start
Box(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
contentAlignment = alignment
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = backgroundColor,
) {
Text(
text = message.text,
modifier = Modifier.padding(8.dp),
color = MaterialTheme.colors.onPrimary
)
}
}
}
@Preview(showBackground = true)
@Composable
fun ChatScreenPreview() {
MaterialTheme {
ChatScreen()
}
}
In this implementation:
ChatScreen
composable contains the overall layout including the message list and input components.- A
LazyColumn
is used for efficiently rendering the list of chat messages in a reversed order (newest messages at the bottom). - The input section comprises a
TextField
for typing messages and anIconButton
to send them. - The
ChatMessageItem
composable is responsible for rendering individual messages with appropriate styling (e.g., background color, alignment based on message source).
Step 4: Enhance with State Management (ViewModel)
To manage the chat UI state more efficiently, integrate a ViewModel
.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ChatViewModel : ViewModel() {
private val _messages = MutableLiveData>(emptyList())
val messages: LiveData> = _messages
fun sendMessage(text: String) {
if (text.isNotBlank()) {
_messages.value = _messages.value.orEmpty() + Message(text, true)
}
}
}
Update the ChatScreen
to use the ChatViewModel
:
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.graphics.Color
@Composable
fun ChatScreen(viewModel: ChatViewModel = viewModel()) {
var messageText by remember { mutableStateOf("") }
val messages: List by viewModel.messages.observeAsState(initial = emptyList())
Column(modifier = Modifier.fillMaxSize()) {
// Message List
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(8.dp),
reverseLayout = true
) {
items(messages.reversed()) { message ->
ChatMessageItem(message = message)
}
}
// Input Field and Send Button
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = messageText,
onValueChange = { messageText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Type a message...") }
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(onClick = {
viewModel.sendMessage(messageText)
messageText = "" // Clear input field
}) {
Icon(Icons.Filled.Send, contentDescription = "Send")
}
}
}
}
Step 5: Implement Styling and Theming
Apply custom theming to match your app’s design:
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun MyApp() {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
ChatScreen()
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyApp()
}
Conclusion
Building a chat UI in Jetpack Compose involves creating composable functions for displaying messages, handling user input, and managing the UI state. By leveraging Compose’s declarative nature and integrating with state management tools like ViewModel
, you can create an efficient and aesthetically pleasing chat interface. The modular design makes it easy to customize and extend the chat UI to meet your specific requirements. With styling and theming, you can make it visually consistent with your app’s brand.