How to build Android In-App Chat with Jetpack Compose

Learn how to use our Android Chat SDK with Jetpack Compose to create a polished messaging experience that includes - typing indicators, read state, attachments, reactions, user presence, and threads.

We'll start with a super quick and simple integration, and then look at some of the flexibility and customization that the Compose SDK offers.

example of android chat sdk

This tutorial teaches you how to build in-app chat or messaging with Jetpack Compose & Stream's edge network.

On the right you see a GIF of how the end result will look, a sample app is also available on our repo.

This chat tutorial uses Stream's edge network for optimal performance. For hobby projects and small companies we provide a free maker plan.

In case you can't use Compose yet in your app, there's also an XML Android chat tutorial available Let's get started with the tutorial and see how quickly you can build chat for your app.

Main Features

These are the main features that our chat app will have:

Installation

Create a New Android Studio Project

To get started with the Jetpack Compose version of the Android Chat SDK, open Android Studio (Giraffe or newer) and create a new project.

Once you create and load the project, you need to add appropriate dependencies for Jetpack Compose. Our SDKs are available from MavenCentral.

Let's add the Stream Chat Compose SDK to the project's dependencies. For the tutorial we will add offline support by adding the stream-chat-android-offline plugin dependency. You'll also add a dependency on material-icons-extended , as you'll use an icon provided there for customization in later steps.

Open up the app module's build.gradle.kts (or build.gradle if you are using older style Groovy DSL) file and add the following three dependencies:

build.gradle.kts (Kotlin DSL) build.gradle (Groovy DSL)
1
2
3
4
5
6
7
8
dependencies implementation("io.getstream:stream-chat-android-compose:6.4.3") implementation("io.getstream:stream-chat-android-offline:6.4.3") implementation("androidx.compose.material:material-icons-extended:1.6.0-alpha08") ... >
1
2
3
4
5
6
7
8
dependencies implementation 'io.getstream:stream-chat-android-compose:6.4.3' implementation 'io.getstream:stream-chat-android-offline:6.4.3' implementation 'androidx.compose.material:material-icons-extended:1.6.0-alpha08' . >

The Compose Chat SDK requires compileSdk version to be set to 34 or higher. Android Studio version Hedgehog or newer will set 34+ automatically for new projects. You can verify the compileSdk version in build.graddle in your project folder (usually named app ):

1
2
3
4
5
. android namespace = "com.example.chattutorial" compileSdk = 34 .

To simplify the process of trying out the sample code you can also copy the following imports into your MainActivity.kt :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material.Text import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.logger.ChatLogLevel import io.getstream.chat.android.compose.ui.channels.ChannelsScreen import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.models.InitializationState import io.getstream.chat.android.models.User import io.getstream.chat.android.offline.plugin.factory.StreamOfflinePluginFactory import io.getstream.chat.android.state.plugin.config.StatePluginConfig import io.getstream.chat.android.state.plugin.factory.StreamStatePluginFactory

You now have an empty project for your chat app with the Stream Jetpack Compose Chat SDK as a dependency. Let's get started by creating the chat client.

Client setup

Step 1: Setup the ChatClient

First, we need to setup the ChatClient . For this, go to the created MainActivity.kt file and add the following code inside the onCreate() function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainActivity : ComponentActivity() override fun onCreate(savedInstanceState: Bundle?) super.onCreate(savedInstanceState) // 1 - Set up the OfflinePlugin for offline storage val offlinePluginFactory = StreamOfflinePluginFactory(appContext = applicationContext) val statePluginFactory = StreamStatePluginFactory(config = StatePluginConfig(), appContext = this) // 2 - Set up the client for API calls and with the plugin for offline storage val client = ChatClient.Builder("uun7ywwamhs9", applicationContext) .withPlugins(offlinePluginFactory, statePluginFactory) .logLevel(ChatLogLevel.ALL) // Set to NOTHING in prod .build() ... >

Step 2: Connect the User

As a next step we're going to connect a user to the chat. The following example will demonstrate the authentication of a user using a JWT token. It's also possible to have anonymous or guest users. For a comprehensive understanding of connecting and authenticating users, refer to the auth & connect docs. In this instance, a harcoded JWT token is employed. In a production application, the JWT token is generally supplied as part of your backend's login and registration endpoints.

Here's our connectUser implementation that authenticates the user. You should call this method right after the setup of the client instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity : ComponentActivity() override fun onCreate(savedInstanceState: Bundle?) super.onCreate(savedInstanceState) ... // 3 - Authenticate and connect the user val user = User( id = "tutorial-droid", name = "Tutorial Droid", image = "https://bit.ly/2TIt8NR" ) client.connectUser( user = user, token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZHJvaWQifQ.WwfBzU1GZr0brt_fXnqKdKhz3oj0rbDUm2DqJO_SS5U" ).enqueue() >

Presenting a channel list

There are 2 ways you can build the UI for the channel list

Let's try how the ChannelsScreen can be easily implemented by adding the following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MainActivity : ComponentActivity() override fun onCreate(savedInstanceState: Bundle?) super.onCreate(savedInstanceState) ... setContent // Observe the client connection state val clientInitialisationState by client.clientState.initializationState.collectAsState() ChatTheme when (clientInitialisationState) InitializationState.COMPLETE -> ChannelsScreen( title = stringResource(id = R.string.app_name), isShowingSearch = true, onChannelClick = channel -> TODO() >, onBackPressed = finish() > ) > InitializationState.INITIALIZING -> Text(text = "Initialising. ") > InitializationState.NOT_INITIALIZED -> Text(text = "Not initialized. ") > > > > >

Here is the full code so far:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.example.chattutorial import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material.Text import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.logger.ChatLogLevel import io.getstream.chat.android.compose.ui.channels.ChannelsScreen import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.models.InitializationState import io.getstream.chat.android.models.User import io.getstream.chat.android.offline.plugin.factory.StreamOfflinePluginFactory import io.getstream.chat.android.state.plugin.config.StatePluginConfig import io.getstream.chat.android.state.plugin.factory.StreamStatePluginFactory class MainActivity : ComponentActivity() override fun onCreate(savedInstanceState: Bundle?) super.onCreate(savedInstanceState) // 1 - Set up the OfflinePlugin for offline storage val offlinePluginFactory = StreamOfflinePluginFactory(appContext = applicationContext,) val statePluginFactory = StreamStatePluginFactory(config = StatePluginConfig(), appContext = this) // 2 - Set up the client for API calls and with the plugin for offline storage val client = ChatClient.Builder("uun7ywwamhs9", applicationContext) .withPlugins(offlinePluginFactory, statePluginFactory) .logLevel(ChatLogLevel.ALL) // Set to NOTHING in prod .build() // 3 - Authenticate and connect the user val user = User( id = "tutorial-droid", name = "Tutorial Droid", image = "https://bit.ly/2TIt8NR" ) client.connectUser( user = user, token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZHJvaWQifQ.WwfBzU1GZr0brt_fXnqKdKhz3oj0rbDUm2DqJO_SS5U" ).enqueue() setContent // Observe the client connection state val clientInitialisationState by client.clientState.initializationState.collectAsState() ChatTheme when (clientInitialisationState) InitializationState.COMPLETE -> ChannelsScreen( title = stringResource(id = R.string.app_name), isShowingSearch = true, onChannelClick = channel -> TODO() >, onBackPressed = finish() > ) > InitializationState.INITIALIZING -> Text(text = "Initializing. ") > InitializationState.NOT_INITIALIZED -> Text(text = "Not initialized. ") > > > > > >

Composable UI Components rely on a ChatTheme being present somewhere above them in the UI hierarchy. Make sure you add this wrapper whenever you're using the components of the Chat SDK. Learn more in the ChatTheme documentation.

Build and run your application - you should see the channel screen interface shown on the right. Notice how easy it was to build a fully-functional screen with Compose!

Internally, the ChannelsScreen uses these smaller components:

If you want to customize what this screen looks like, or how it behaves, you can build it from these individual components. See the Component Architecture page of the documentation for more details.

Now that you can display channels, let's open up one of them and start chatting!

Presenting a channel

To start chatting, you need to build another screen - the Channel Screen.

Create a new Empty Activity (New -> Compose -> Empty Activity) and name it ChannelActivity .

Make sure that ChannelActivity is added to your manifest. Android Studio does this automatically if you use the wizard to create the Activity, but you'll need to add it yourself if you manually created the Activity class.

After creating the Activity, add the following attribute to the ChannelActivity entry in your AndroidManifest.xml :

1
android:windowSoftInputMode="adjustResize"

This will make sure the Activity adjusts properly when you focus the input field.

Next, replace the code in ChannelActivity with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.chattutorial import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import io.getstream.chat.android.compose.ui.messages.MessagesScreen import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory class ChannelActivity : ComponentActivity() override fun onCreate(savedInstanceState: Bundle?) super.onCreate(savedInstanceState) // 1 - Load the ID of the selected channel val channelId = intent.getStringExtra(KEY_CHANNEL_ID)!! // 2 - Add the MessagesScreen to your UI setContent ChatTheme MessagesScreen( viewModelFactory = MessagesViewModelFactory( context = this, channelId = channelId, messageLimit = 30 ), onBackPressed = finish() > ) > > > // 3 - Create an intent to start this Activity, with a given channelId companion object private const val KEY_CHANNEL_ID = "channelId" fun getIntent(context: Context, channelId: String): Intent return Intent(context, ChannelActivity::class.java).apply putExtra(KEY_CHANNEL_ID, channelId) > > > >

Let's review what's going on in this snippet:

  1. You load the channelId from the Intent extras. If there is no channel ID, you can't show messages, so you finish the Activity and return. Otherwise, you can proceed to render the UI.
  2. Similar to the ChannelsScreen, the MessagesScreen component sets up everything for you to show a list of messages and build a Chat experience. Note how this screen's composable should also wrapped in ChatTheme . The MessagesScreens requires a MessagesViewModelFactory instance - you can learn more about it in our documentation.
  3. Set up a helper function to build an Intent for this Activity , that populates the arguments with the channel ID.

Lastly, you want to launch ChannelActivity when you tap a channel in the channel list. Open MainActivity and replace the TODO() within onChannelClick with the following:

1
2
3
onItemClick = channel -> startActivity(ChannelActivity.getIntent(this, channel.cid)) >,

Run the application and tap on a channel: you'll now see the chat interface shown on the right.

The MessagesScreen component is the second screen component in the SDK, out of the box it provides you with the following features:

You can explore all of these components individually, combine them to your requirements, and explore the Compose UI Components documentation to see how they behave and how you can customize them.

More complex code samples can be also found in our GitHub sample repository:

With our Android Chat SDK, the possibilities customisation possibilities are limitless. Customize your user experience effortlessly through theming, build unique components from scratch, or mix and match existing elements to suit your app's personality and functionality.

Theming

To change the theming of all the components wrapped by ChatTheme , all you have to do is override its default parameters. Let's do that, with the shapes parameter. Change the setContent() code in ChannelActivity.kt to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
setContent ChatTheme( shapes = StreamShapes.defaultShapes().copy( avatar = RoundedCornerShape(8.dp), attachment = RoundedCornerShape(16.dp), myMessageBubble = RoundedCornerShape(16.dp), otherMessageBubble = RoundedCornerShape(16.dp), inputField = RectangleShape, ) ) MessagesScreen( viewModelFactory = MessagesViewModelFactory( context = this, channelId = channelId, messageLimit = 30 ), onBackPressed = finish() > ) > >

With this small change, you can override the default shapes used in our Compose UI Components.

You made the following changes:

Notice how you changed the theme shapes using copy() . For ease of use, you can fetch the default theme values and use copy() on the data class to change just the properties you want to customize.

If you build the app now and open the messages screen, you'll see how the messages are all rounded, the input is rectangular and the avatars are now a squircle! That was easy!

Combining components

The next step of customization is combining our bound and stateless components instead of using the screen components. This gives you more control over which components you use and render and what the behavior is when tapping on items, selecting messages and more.

You can inspect how the ChannelActivity can be customised in our GitHub sample here. In this example we replace the high-level component MessagesScreen with a custom UI build from our UI components instead.

Video / audio room integration

For a complete social experience, Stream provides a Video & Audio calling Android SDK, that works seamlessly with our chat products. If you want to learn more on how to integrate video into your apps, please check our docs and our tutorials about video calling and livestreaming.

Additionally, we provide a guide on seamlessly integrating video with chat.

Final Thoughts

We've guided you through crafting a feature-rich, in-app chat experience with Android Jetpack Compose — complete with reactions, threads, typing indicators, offline storage, URL previews, user presence, and more. It's astonishing how APIs and Compose components empower you to bring a chat to life in just hours. Beyond that, you've learned how effortlessly you can add your personal touch with custom themes and fully tailor key components.

Our chat app leverages Stream's edge network, ensuring optimal performance and scalability, supporting thousands of apps and over a billion end users. For development, there's a free plan, and for hobby projects and small apps, we offer a more extensive free maker plan. Check the available price tiers.

Both the Chat SDK for Compose and the API boast a plethora of additional features for advanced use-cases, including push notifications, content moderation, rich messages, and more. Furthermore, we've demonstrated how to tap into our low-level state from the chat client for those inclined to craft their own custom messaging experiences.

Give us feedback!

Did you find this tutorial helpful in getting you up and running with your project? Either good or bad, we're looking for your honest feedback so we can improve.