Learn how to quickly integrate rich Generative AI experiences directly into Stream Chat. Learn More ->

Android Chat Messaging Tutorial

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, an XML Android chat tutorial is also available. Let's start 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:

  • Channel List: Browse channels and perform actions such as search and swipe-to-delete.
  • Message Composer: Customizable and expandable with bespoke implementation.
  • Message Reactions: Ready-made and easily configurable.
  • Offline Support: Browse channels and send messages while offline.
  • Customizable Components: Build quickly with customizable and swappable building blocks.

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.

  • Select the Empty Activity template.
  • Name the project ChatTutorial.
  • Set the package name to com.example.chattutorial.

Once you create and load the project, you must 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:

kotlin
1
2
3
4
5
6
7
8
dependencies { implementation(libs.stream.chat.android.compose) implementation(libs.stream.chat.android.offline) implementation(libs.androidx.material.icons.extended) ... }

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):

groovy
1
2
3
4
5
6
... 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:

kotlin
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 start 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:

kotlin
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) // 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() ... } }
  • You create a StreamOfflinePluginFactory to provide offline support. The OfflinePlugin class employs a new caching mechanism powered by side-effects we applied to ChatClient functions.
  • You create a connection to Stream by initializing the ChatClient using an API key. This key points to a tutorial environment, but you can sign up for a free Chat trial to get your own later.
  • Next, we add the offlinePluginFactory to the ChatClient with withPlugin method for providing offline storage capabilities. For a production app, we recommend initializing this ChatClient in your Application class.

Step 2: Connect the User

As a next step, we'll connect a user to the chat. The following example will demonstrate how a user can be authenticated 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 hardcoded JWT token is used. 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, which authenticates the user. You should call this method right after setting up the client instance.

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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() } }
  • You create a User instance and pass it to the ChatClient's connectUser method and a pre-generated user token to authenticate the user. In a real-world application, your authentication backend would generate a token at login/signup and hand it over to the mobile app. For more information, see the Tokens & Authentication page.
  • The connectUser.enqueue() is an asynchronous function with a result callback; in Kotlin, you can use the suspending function by using await() instead. Executing the function synchronously with execute() is also possible. Our Calls section explains this in more detail.

Presenting a Channel List

There are two ways you can build the UI for the channel list.

  • Stream's low-level API. You can build a custom UI on top of this state layer.
  • Or you can use some of our pre-made UI components. Most customers mix and match these two approaches to meet their design requirements.

Let's try how the ChannelsScreen can be easily implemented by adding the following code to the MainActivity.kt file:

kotlin
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
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), isShowingHeader = true, onChannelClick = { channel -> TODO() }, onBackPressed = { finish() } ) } InitializationState.INITIALIZING -> { Text(text = "Initialising...") } InitializationState.NOT_INITIALIZED -> { Text(text = "Not initialized...") } } } } } }
  • Observe the Chat SDK initialization state (client. client state.initializationState StateFlow) to ensure that the SDK is correctly initialized and the user has been set. Note that this doesn't mean that the SDK is online and connected—for that, you can observe the client.clientState.connectionState.
  • Call the ChannelsScreen composable function to render the entire ChannelsScreen and wrap it in our theme, which provides styling options. You also pass in the app name as the screen's title and an onBackPressed listener.

Here is the full code so far:

kotlin
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), isShowingHeader = 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:

  • ChannelListHeader: Displays information about the user, app and exposes a header action.
  • SearchInput: Displays an input field, to query items. Exposes value change handlers to query new items from the API.
  • ChannelList: Displays a list of Channel items in a paginated list and exposes single and long tap actions on items.

If you want to customize this screen's look or behavior, you can build it from these individual components. For more details, see the Component Architecture page of the documentation.

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

Presenting a Channel

To start chatting, you must 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 create the Activity class.

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

xml
1
android:windowSoftInputMode="adjustResize"

The above will ensure the Activity adjusts properly when the input field is in focus.

Next, replace the code in ChannelActivity with the following:

kotlin
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:

kotlin
1
2
3
onChannelClick = { channel -> startActivity(ChannelActivity.getIntent(this, channel.cid)) },

Run the application and tap on a channel. The chat interface will now appear 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:

  • Header: Displays a back button, the name of the channel or thread, and a channel avatar.
  • Messages: Shows a paginated list of messages if the data is available; otherwise, it displays the correct empty or loading state. It sets up action handlers and displays a button for a quick scroll to the bottom action.
  • Message composer: It handles message input, attachments, and actions like editing, replying, and more.
  • Attachment picker: It allows users to select images, files, and media capture.
  • Message options: Shown when the user selects the message by long tapping. Allows the user to react to messages and perform different actions such as deleting, editing, replying, starting a thread, and more.
  • Reactions menu: This is shown when the user taps on a reaction to a message. It displays all of the reactions left on the message, along with the option to add or change yours.

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 also be found in our GitHub sample repository:

  • MessagesActivity3 - uses bound and stateless components to build the chat screen, with further customization
  • MessagesActivity4 - uses a custom message composer component for extended customization

With our Android Chat SDK, customization possibilities are limitless. You can effortlessly customize your user experience 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, you just have to override its default parameters. Let's do that with the shapes parameter. Change the setContent() code in ChannelActivity.kt to the following:

kotlin
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:

  • Instead of having rounded corners, you made the inputField rectangular.
  • The owned and other people's messages will be rounded on all corners, regardless of their position in a message group
  • Avatars and attachments have rounded corners of 8dp and 16dp, respectively.

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 the properties you want to customize.

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

Combining Components

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

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

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 about integrating 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 — 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 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. It supports thousands of apps and over a billion end users. There's a free development 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.

Start coding for free

No credit card required.
If you're interested in a custom plan or have any questions, please contact us.