Build an Android Chat app with Jetpack Compose
Tags: android, chat, kotlin, jetpackcompose
Published: 2021-03-19 13:42:18

## Intro and context

In [our previous article](https://dev.to/zsmb13/jetpack-compose-first-impressions-and-learning-resources-3ghp) about Jetpack Compose, we shared our initial impressions and recommended some learning resources. This time, we'll take Compose out for a spin by building a basic chat UI implementation with it!

> Stream now provides a [Jetpack Compose Chat SDK](https://getstream.io/chat/sdk/compose/). Check out the [Compose Chat Messaging Tutorial](https://getstream.io/chat/compose/tutorial/) and give it a try today!

Of course, you might be wondering why we're building a Compose Chat app when there's an official Google sample, [Jetchat](https://github.com/android/compose-samples/tree/main/Jetchat), which is also a chat app using Compose. That sample is a basic UI demo with a small bit of hardcoded data. Here, we're building a real, functional chat app instead that's hooked up to a real-time backend service.

We already ship a [UI Components library](https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-components) in our Chat SDK which contains ready-to-go, fully-featured Android Views to drop into your app, along with ViewModels that let you connect them to business logic in just a single line of code. You can check out how that works in our [Android Chat Tutorial](https://getstream.io/tutorials/android-chat/).

![The Stream Chat UI Components in action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eubnxjmycyivkjos9vnv.png)

This time, we're going to reuse the ViewModels (and a View) from that implementation, and build our Compose-based UI on top of that. This won't be an ideal scenario, as those ViewModels are designed to work with the Views that they ship with.

Having dedicated support for Compose in the SDK would make this much nicer and simpler, and that's something we'll be working on in the future. However, as you'll see, it's already quite easy to integrate Stream Chat with Jetpack Compose, without any specific support for it!

![The Compose based chat implementation we'll build](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tf4wy0j13eac8hyim9xr.png)

The project we're building in this article is  [available on GitHub](https://github.com/zsmb13/stream-chat-compose-experiment), feel free to clone the project and play around with it on your own! We recommend getting your own Stream API key for this, which you can do by [signing up for a free trial](https://getstream.io/try-for-free/). Stream is free for hobby projects and small companies even beyond the trial - just apply for a [free Maker account](https://getstream.io/maker-account/).

## Project setup

To use Jetpack Compose, we'll create our project in the [latest Canary version of Android Studio](https://developer.android.com/studio/preview). This contains a project template for Compose apps.

![Jetpack Compose project template in Android Studio](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86fmvejfrtfbkph4ap9v.png)

After creating the project, our first step will be to add the Stream UI Components SDK as a dependency, as well as some Jetpack Compose dependencies we need. Some of our dependencies come from Jitpack, so make sure to add that repository in the `settings.gradle` file:

```gradle
dependencyResolutionManagement {
    repositories {
        ...
        maven {url "https://jitpack.io" } // Add Jitpack
    }
}
```

Then, in the module's `build.gradle` file, replace the `dependencies` block with the following:

```gradle
dependencies {
    // Stream Chat UI Components
    implementation "io.getstream:stream-chat-android-ui-components:4.8.1"

    // Google / Jetpack dependencies
    implementation "androidx.compose.runtime:runtime:$compose_version"
    implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'
    implementation "androidx.navigation:navigation-compose:1.0.0-alpha10"
    implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04'
}
```

*Check out [the GitHub project](https://github.com/zsmb13/stream-chat-compose-experiment) if you get tangled up in the dependencies.*

Next, we'll set up the Stream Chat SDK, providing an API key and user details to it. This is normally done in your custom `Application` class. To make things simple, we'll use the same environment and user as [our tutorial](https://getstream.io/tutorials/android-chat/).

```kotlin
class ChatApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val client = ChatClient.Builder(appContext = this, apiKey = "b67pax5b2wdq").build()

        val user = User().apply {
            id = "tutorial-droid"
            image = "https://bit.ly/2TIt8NR"
            name = "Tutorial Droid"
        }

        ChatDomain.Builder(client, this)
            .offlineEnabled()
            .build()

        client.connectUser(
            user,
            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZHJvaWQifQ.NhEr0hP9W9nwqV7ZkdShxvi02C5PR7SJE7Cs4y7kyqg"
        ).enqueue()
    }
}
```

We're initializing all three layers of our SDK here: the low-level client, the domain providing offline support, and the UI components. For more info about these, take a look at [the documentation](https://getstream.io/chat/docs/android/?language=kotlin). Finally, we're connecting our user, using a hardcoded, non-expiring example token from the tutorial.

Since we created a custom `Application` class, we have to update our Manifest to use this `ChatApplication` when the app is launched. We'll also set `windowSoftInputMode` on the `MainActivity` so that we'll have better keyboard behaviour later on.

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <application
        android:name=".ChatApplication"
        ...>
        <activity
            android:name=".MainActivity"
            ...
            android:windowSoftInputMode="adjustResize">
            ...
        </activity>
    </application>
</manifest>
```

## Channel List Screen

First, we'll create a screen that lists the channels available to our user. The UI Components library contains a [ChannelListView component](https://getstream.io/chat/docs/android/channel_list_view/?language=kotlin) for this purpose, and we can reuse its ViewModel here.

```kotlin
@Composable
fun ChannelListScreen(
    channelListViewModel: ChannelListViewModel = viewModel( // 1
        factory = ChannelListViewModelFactory()
    ),
) {
    val state by channelListViewModel.state.observeAsState()
    val channelState = state ?: return // 2

    Box( // 3
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center,
    ) {
        if (channelState.isLoading) {
            CircularProgressIndicator() // 4
        } else {
            LazyColumn(Modifier.fillMaxSize()) { // 5
                items(channelState.channels) { channel ->
                    ChannelListItem(channel)
                    Divider()
                }
            }
        }
    }
}
```

Let's see what this code does step-by-step:

1. We use the `viewModel()` method provided by Compose to get a ViewModel for our current context. This allows us to specify a factory that creates a ViewModel, which we need in the case of `ChannelListViewModel` to pass in some parameters to it. We'll stick to the default parameters of the SDK's `ChannelListViewModelFactory` for now, which will filter for channels that our current user is a member of.
2. We observe the `LiveData` state in the ViewModel as a composable `State`, and we copy it to a local variable so that smart casts will work on it correctly. We also skip rendering anything if this `State` happens to be `null`.
3. We'll have the `ChannelListScreen` fill the entire screen, and center any content inside it.
4. If the state is loading, we'll display a basic progress indicator.
5. Otherwise, we'll display the list of channels in a `LazyColumn`. Each item will be a `ChannelListItem` responsible for showing info for a single channel. We're also adding a simple `Divider` after each item.

Now, let's see how we can render a single `ChannelListItem` in the list:

```kotlin
@Composable
fun ChannelListItem(channel: Channel) {
    Row( // 1
        modifier = Modifier
            .padding(horizontal = 8.dp, vertical = 8.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Avatar(channel) // 2
        Column(modifier = Modifier.padding(start = 8.dp)) { // 3
            Text(
                text = channel.getDisplayName(LocalContext.current),
                style = TextStyle(fontWeight = FontWeight.Bold),
                fontSize = 18.sp,
            )

            val lastMessageText = channel.messages.lastOrNull()?.text ?: "..."
            Text(
                text = lastMessageText,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
        }
    }
}
```

1. We're rendering each item as a `Row`.
2. Inside that, we're adding an `Avatar` representing the `Channel` at the start.
3. Then we add a `Column`, laying out two pieces of `Text` vertically. These will contain the channel's title and a preview of the latest message. Note the `LocalContext.current` API being used to grab a `Context` in Compose, as well as the `TextOverflow.Ellipsis` option that will make long messages behave nicely in the preview.

We have one last Composable to implement, the `Avatar` used above. Our [UI components SDK](https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-components) ships an `AvatarView` with complex rendering logic inside it, which we don't want to reimplement for now. We can use [Jetpack Compose's interop features](https://developer.android.com/jetpack/compose/interop) to include our regular Android `View` inside Compose UI.

This will be really simple, just a few lines of code to create a wrapper:

```kotlin
@Composable
fun Avatar(channel: Channel) {
    AndroidView(
        factory = { context -> AvatarView(context) },
        update = { view -> view.setChannelData(channel) },
        modifier = Modifier.size(48.dp)
    )
}
```

The `AndroidView` function allows us to create... Well, an Android `View`, which we get a `Context` for. It also gives us a way to hook into Compose's state updates using its `update` parameter, which will be executed whenever composable state (in our case, the `Channel` object) changes. When that happens, we set the new `Channel` on our `AvatarView`.

Finally, we can add the `ChannelListScreen` to `MainActivity`:

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeChatTheme {
                Surface(color = MaterialTheme.colors.background) {
                    ChannelListScreen()
                }
            }
        }
    }
}
```

Building and running this code gives us a working list of channels loaded from Stream's backend, with performant scrolling thanks to `LazyColumn`. Not bad for a hundred lines of code for the entire thing!

![The Channel List screen showing the list of available channels](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pn8rntay6kbs06fn3cnr.png)

## Setting up navigation

Continuing with our chat app, let's create a new screen, where we'll display the list of messages in a channel. For this, we'll need to set up click listeners and navigation. We'll use the [Navigation Component for Jetpack Compose](https://developer.android.com/jetpack/compose/navigation) here.

Step one, we'll make our `ChannelListItem` clickable, and add an `onClick` parameter that it will call when it was clicked:

```kotlin
@Composable
fun ChannelListItem(
    channel: Channel,
    onClick: (channel: Channel) -> Unit, // New callback parameter
) {
    Row(
        modifier = Modifier
            .clickable { onClick(channel) } // Add clickablity
            .padding(horizontal = 8.dp, vertical = 8.dp)
            .fillMaxWidth(),
        ...
    ) { ... }
}
```

`ChannelListScreen` will take a `NavController` as its parameter, and use it to navigate to a new destination when an item was clicked, based on the `cid` of the channel.

```kotlin
@Composable
fun ChannelListScreen(
    navController: NavController, // New parameter
    channelListViewModel: ChannelListViewModel = viewModel(
        factory = ChannelListViewModelFactory()
    ),
) {
    ...
        items(channelState.channels) { channel ->
            ChannelListItem(
                channel = channel,
                onClick = { navController.navigate("messagelist/${channel.cid}") },
            )
            Divider()
        }
    ...
}
```

We'll update `MainActivity` by moving our top-level Composable code to a `ChatApp` composable, and setting up navigation in there.

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeChatTheme {
                Surface(color = MaterialTheme.colors.background) {
                    ChatApp()
                }
            }
        }
    }
}

@Composable
fun ChatApp() {
    val navController = rememberNavController() // 1
    NavHost(navController, startDestination = "channellist") {
        composable("channellist") { // 2
            ChannelListScreen(navController = navController)
        }
        composable("messagelist/{cid}") { backStackEntry -> // 3
            MessageListScreen(
                navController = navController,
                cid = backStackEntry.arguments?.getString("cid")!!
            )
        }
    }
}
```

1. We're creating a `NavController` and a `NavHost` for our application.
2. Our first and initial destination is a composable: `ChannelListScreen`. We pass the nav controller to it.
3. Our second destination will be a new composable called `MessageListScreen`. Here, we pass in both the nav controller and the argument that was used to navigate to it. We can grab this from the `NavBackStackEntry` parameter.

## Message List Screen

Our message list will be a `LazyColumn` similar to the channel list we created before. For displaying messages, the UI Components library gives us a `MessageListViewModelFactory` which can create a few different ViewModel instances. This takes the `cid` of the channel we want to display.

```kotlin
@Composable
fun MessageListScreen(
    navController: NavController,
    cid: String,
) {
    val factory = MessageListViewModelFactory(cid)
    MessageList(
        navController = navController,
        factory = factory,
    )
}
```

We'll implement the `MessageList` like this:

```kotlin
@Composable
fun MessageList(
    navController: NavController,
    factory: MessageListViewModelFactory,
    modifier: Modifier = Modifier,
    messageListViewModel: MessageListViewModel = viewModel(factory = factory), // 1
) {
    val state by messageListViewModel.state.observeAsState() // 2
    val messageState = state ?: return

    Box(
        modifier = modifier,
        contentAlignment = Alignment.Center
    ) {
        when (messageState) { // 3
            is MessageListViewModel.State.Loading -> {
                CircularProgressIndicator()
            }
            is MessageListViewModel.State.NavigateUp -> {
                navController.popBackStack()
            }
            is MessageListViewModel.State.Result -> {
                val messageItems = messageState.messageListItem.items // 4
                    .filterIsInstance<MessageListItem.MessageItem>()
                    .filter { it.message.text.isNotBlank() }
                    .asReversed()

                LazyColumn(
                    modifier = Modifier.fillMaxSize(),
                    reverseLayout = true, // 5
                ) {
                    items(messageItems) { message ->
                        MessageCard(message) // 6
                    }
                }
            }
        }
    }
}
```

Let's review:

1. We get a `MessageListViewModel` from the factory, which will fetch messages for us, and expose it as `LiveData`.
2. As before, we convert the `LiveData` state from the ViewModel into composable `State`.
3. We have three states to handle, defined by a sealed class inside `MessageListViewModel`. The first state is a loading state, the second is a state that pushes us to the previous screen (using the nav controller), and the third is the result state, where we have a list of messages.
4. In the result state, we'll grab the list of messages received, filter for only `MessageItem`s (excluding things such as date separators that we don't want to render for now). We also filter for messages that have non-blank text - for example, some messages might have only images attached to them, so we'll skip those for simplicity. Finally, we reverse the list, to match what we're doing in the next step.
5. We use `reverseLayout` on `LazyColumn` to stack its items from the bottom. This is similar to using `stackFromEnd` on a `RecyclerView`.
6. Each item will be rendered by a `MessageCard` composable.

For a single message, we'll use the following layout:

```kotlin
@Composable
fun MessageCard(messageItem: MessageListItem.MessageItem) { // 1
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp, vertical = 4.dp),
        horizontalAlignment = when { // 2
            messageItem.isMine -> Alignment.End
            else -> Alignment.Start
        },
    ) {
        Card(
            modifier = Modifier.widthIn(max = 340.dp),
            shape = cardShapeFor(messageItem), // 3
            backgroundColor = when {
                messageItem.isMine -> MaterialTheme.colors.primary
                else -> MaterialTheme.colors.secondary
            },
        ) {
            Text(
                modifier = Modifier.padding(8.dp),
                text = messageItem.message.text,
                color = when {
                    messageItem.isMine -> MaterialTheme.colors.onPrimary
                    else -> MaterialTheme.colors.onSecondary
                },
            )
        }
        Text( // 4
            text = messageItem.message.user.name,
            fontSize = 12.sp,
        )
    }
}

@Composable
fun cardShapeFor(message: MessageListItem.MessageItem): Shape {
    val roundedCorners = RoundedCornerShape(16.dp)
    return when {
        message.isMine -> roundedCorners.copy(bottomEnd = CornerSize(0))
        else -> roundedCorners.copy(bottomStart = CornerSize(0))
    }
}
```

This code is mostly straightforward, but let's review some of its important bits:

1. We take a `MessageItem` as a parameter.
2. Depending on whether this is our current user's message, or someone else's, we align it to one of the sides of the screen. We also set colours based on this in a few places.
3. We use a `cardShapeFor` helper method to create the shape of the `Card` that holds the message. This will create a shape with rounded corners, except for one of the bottom corners, giving us a chat bubble look.
4. We display the username under each message, so that we can distinguish between messages sent by others.

At this point, we can build and run again, and clicking a channel will navigate to the new screen, displaying the list of messages in it.

![The Message List Screen with a list of messages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sx0iw0yrftjljqb7q3ah.png)

## Message Input

For our final piece of Jetpack Compose chat implementation, we'll add an input view on the message list screen so that we can send new messages.

First, we'll modify `MessageListScreen` and place a new composable under `MessageList`:

```kotlin
@Composable
fun MessageListScreen(
    navController: NavController,
    cid: String,
) {
    Column(Modifier.fillMaxSize()) {
        val factory = MessageListViewModelFactory(cid)

        MessageList(
            navController = navController,
            factory = factory,
            modifier = Modifier.weight(1f),
        )
        MessageInput(factory = factory)
    }
}
```

The `weight` modifier on `MessageList` will make it take up the maximum available space above the new `MessageInput`. We pass in the `factory` to `MessageInput` as well, so that it's able to access the currently open channel's data via ViewModels.

```kotlin
@Composable
fun MessageInput(
    factory: MessageListViewModelFactory,
    messageInputViewModel: MessageInputViewModel = viewModel(factory = factory), // 1
) {
    var inputValue by remember { mutableStateOf("") } // 2

    fun sendMessage() { // 3
        messageInputViewModel.sendMessage(inputValue)
        inputValue = ""
    }

    Row {
        TextField( // 4
            modifier = Modifier.weight(1f),
            value = inputValue,
            onValueChange = { inputValue = it },
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
            keyboardActions = KeyboardActions { sendMessage() },
        )
        Button( // 5
            modifier = Modifier.height(56.dp),
            onClick = { sendMessage() },
            enabled = inputValue.isNotBlank(),
        ) {
            Icon( // 6
                imageVector = Icons.Default.Send,
                contentDescription = stringResource(R.string.cd_button_send)
            )
        }
    }
}
```

1. This time, we'll use a `MessageInputViewModel`, which normally belongs to the [MessageInputView](https://getstream.io/chat/docs/android/message_input_view/?language=kotlin) shipped in the UI components library, and handles input actions.
2. We create a piece of composable state to hold the current input value.
3. This local helper function calls into the ViewModel and sends the message to the server. Then, it clears the input field by resetting the state.
4. We use a `TextField` to capture user input. This displays the current `inputValue`, and modifies it based on keyboard input. We also set up IME options so that our software keyboard displays a *Send* button, and we handle that being tapped by calling `sendMessage`.
5. We're also adding a `Button` that the user can tap to send a message. This is enabled/disabled dynamically based on the current input value.
6. The button will show an icon from the default Material icon set. Not to skimp on accessibility, we also add a content description string for the send icon. We grab this from regular Android resources using `stringResource`, which allows us to localize it as usual. *Make sure to create this resource in your project.*

Let's build and run for the last time, and we have a working chat app now! We can browse channels, open them, read messages, and send new messages.

![The Message List Screen, with input support added](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4yfm0wlq73tmxm0xj41c.png)

## Conclusion

This complete implementation, including UI, logic, and a connection to a real server is roughly 250 lines of code in total. Almost all of this is UI code with Jetpack Compose, the integration with Stream Chat's ViewModels and Views is just a small fraction of it.

*Follow us on Twitter [@getstream_io](https://twitter.com/getstream_io), and the author [@zsmb13](https://twitter.com/zsmb13) for more content like this. If you liked this tutorial, tweet at us and let us know!*

As a reminder, you can check out the full project and play with it [on GitHub](https://github.com/zsmb13/stream-chat-compose-experiment). We recommend expanding this project to learn more about Compose - a good first goal could be to add a login screen, so that you can have different users on different devices.

To jump into that, sign up for a [free trial of Stream Chat](https://getstream.io/try-for-free/). If you're doing this for a side project or you're a small business, you can use our SDK for with a [free Maker account](https://getstream.io/blog/maker-account/), even beyond the trial period!

Here are some more useful links to continue exploring:

- The [Stream Chat Android SDK on GitHub](https://github.com/GetStream/stream-chat-android), including the [UI Components Sample app](https://github.com/GetStream/stream-chat-android/tree/main/stream-chat-android-ui-components-sample) that showcases many features, and includes a login implementation you can take ideas from
- Our regular, non-Compose [Android Tutorial](https://getstream.io/tutorials/android-chat/)
- The [Stream Chat Android documentation](https://getstream.io/chat/docs/android/?language=kotlin)
- For more about Compose, some [first impressions and recommended learning resources](https://dev.to/zsmb13/jetpack-compose-first-impressions-and-learning-resources-3ghp)

---

Originally published on [getstream.io/blog](https://getstream.io/blog/android-jetpack-compose-chat-example/).