Jetpack Compose: Building a Chat UI

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 an IconButton 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.