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

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

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

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

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

kotlin
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() ... }
  • 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'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.

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) ... // 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, along with a pre-generated user token, in order to authenticate the user. In a real-world application, your authentication backend would generate such 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 asychronous function with a result callback, in Kotlin you can also take advantage of suspending function by using await() instead. It is also possible to execute the function synchronously with execute(). This is explained in more detail in our Calls section.

Presenting a channel list

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

  • Stream's low level API. You can build custom UI on top of this state layer
  • Or you can use some of our pre-made UI components. Most customers end up mixing and matching between these 2 approaches to meet their design requirements

Let's try how the ChannelsScreen can be easily implemented by adding 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
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...") } } } } }
  • You observe the Chat SDK initialisation state (client.clientState.initializationState StateFlow) to make sure that the SDK is correctly initialised 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.
  • You 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), 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:

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

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:

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

  • 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 displays the correct empty or loading state. Sets up action handlers and displays a button for quick scroll to bottom action.
  • Message composer: Handles message input, attachments and message actions like editing, replying and more.
  • Attachment picker: Allows the user 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: Shown when the user taps on a reaction to a message. 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 be also 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, 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:

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:

  • You made the inputField rectangular, instead of having rounded corners.
  • 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 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.

Start coding for free

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