TUTORIAL

How to Build Android Chat In-App Messaging with Java or Kotlin

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

Let's start working on everything you need for building in-app messaging for your Android chat app.

Android Chat SDK Setup: Java or Kotlin

You can use the Android Chat SDK with both Java or Kotlin. If you don't have a requirement to use Java we recommend Kotlin. If you run into any issues while following this tutorial simply click the icon to leave feedback.

Add Stream Chat to Your Application Example

In order to follow this in-app messaging tutorial, you will need to ensure that Android Studio is installed.

To get started with the Android Chat SDK, open Android Studio and create a new project.

  • Select Empty Activity
  • Rename the project ChatTutorial
  • Ensure the package name is com.example.chattutorial
  • Select your language - Kotlin or Java
  • Select Minimum API level to 28 (Chat SDK is compatible with API level 21 or higher)
  • Enable “Use androidx.*artifacts” if it isn’t already enabled.
Set up demo messaging app in Android Studio

Next, we are going to add the StreamChat SDK to our project dependencies. Open the project build.gradle file and replace its content with this so that we can use jitpack.io to install our requirements.

buildscript {
    ext.kotlin_version = '1.3.61'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'      
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
+       maven {
+           url "https://jitpack.io"
+       }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Next step is to change the application build.gradle script. Here we are going to add StreamChat, Glide, Lifecycle extensions, and the androidx recyclerview to the dependencies. We'll also enable lambda expressions by targeting Java 8 and enable data binding.

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
+    compileSdkVersion 28
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.example.chattutorial"
        minSdkVersion 28
+        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

+    dataBinding {
+        enabled = true
+    }

+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
}

dependencies {
+    implementation 'com.github.GetStream:stream-chat-android:4.0.13'
+    implementation 'com.github.bumptech.glide:glide:4.9.0'
+    implementation 'androidx.recyclerview:recyclerview:1.1.0'
+    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61" //only needed if you're using java

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

Note: When you save your gradle files, make sure to sync the project (Android Studio will prompt you for this) with the new changes.

Channel List

Stream provides a low-level client, livedata objects and custom views to help you quickly build your messaging interface. (We're also thinking about adding RXJava support, so be sure to reach out if that's something you're interested in)

Let's get started by rendering a list of channels. Open your activity_main.xml. In Android studio you can find it under res->layout->activity_main.xml. Change the contents of the file to this:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.getstream.sdk.chat.viewmodel.ChannelListViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.example.chattutorial.MainActivity">


        <com.getstream.sdk.chat.view.ChannelListView
            android:id="@+id/channelList"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginBottom="10dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:streamReadStateAvatarHeight="15dp"
            app:streamReadStateAvatarWidth="15dp"
            app:streamReadStateTextSize="9sp"
            app:streamShowReadState="true" />

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:isGone="@{!safeUnbox(viewModel.loading)}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ProgressBar
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_marginBottom="16dp"
            app:isGone="@{!safeUnbox(viewModel.loadingMore)}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Note how the above layout renders a ChannelListView and 2 progress bars. As a next step open up MainActivity (java->com.example.chattutorial->MainActivity) and replace it with the following code:

package com.example.chattutorial;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProvider;

import android.content.Intent;
import android.util.Log;
import android.widget.Toast;

import com.example.chattutorial.databinding.ActivityMainBinding;
import com.getstream.sdk.chat.Chat;
import com.getstream.sdk.chat.viewmodel.ChannelListViewModel;

import io.getstream.chat.android.client.models.Filters;

import io.getstream.chat.android.client.ChatClient;
import io.getstream.chat.android.client.errors.ChatError;
import io.getstream.chat.android.client.logger.ChatLogLevel;
import io.getstream.chat.android.client.models.User;
import io.getstream.chat.android.client.socket.InitConnectionListener;
import io.getstream.chat.android.client.utils.FilterObject;

public class MainActivity extends AppCompatActivity {
    public static final String EXTRA_CHANNEL_TYPE = "com.example.chattutorial.CHANNEL_TYPE";
    public static final String EXTRA_CHANNEL_ID = "com.example.chattutorial.CHANNEL_ID";

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        // setup the client using the example API key
        // normally you would call init in your Application class and not the activity
        Chat chat = new Chat.Builder("b67pax5b2wdq", getApplicationContext()).logLevel(
                ChatLogLevel.ALL).build();

        ChatClient client = chat.getClient();

        User user = new User("summer-brook-2");
        user.getExtraData().put("name", "Paranoid Android");
        user.getExtraData().put("image", "https://bit.ly/2TIt8NR");
        // User token is typically provided by your server when the user authenticates
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VtbWVyLWJyb29rLTIifQ.CzyOx8kgrc61qVbzWvhV1WD3KPEo5ZFZH-326hIdKz0";
        client.setUser(user, token, new InitConnectionListener() {
            @Override
            public void onSuccess(ConnectionData data) {
                Log.i("MainActivity", "setUser completed");
            }

            @Override
            public void onError(ChatError error) {
                Toast.makeText(getApplicationContext(), error.toString(), Toast.LENGTH_LONG).show();
                Log.e("MainActivity", "setUser onError");
            }
        });
        // we're using data binding in this example
        ActivityMainBinding binding =
                DataBindingUtil.setContentView(this, R.layout.activity_main);
        // Specify the current activity as the lifecycle owner.
        binding.setLifecycleOwner(this);

        // most the business logic for chat is handled in the ChannelListViewModel view model
        ChannelListViewModel viewModel = new ViewModelProvider(this).get(ChannelListViewModel.class);
        binding.setViewModel(viewModel);
        binding.channelList.setViewModel(viewModel, this);

        // query all channels of type messaging
        FilterObject filter = Filters.INSTANCE.and(Filters.INSTANCE.eq("type", "messaging"), Filters.INSTANCE.in("members", "twilight-lab-0"));
        viewModel.setChannelFilter(filter);

        // click handlers for clicking a user avatar or channel
        binding.channelList.setOnChannelClickListener(channel -> {
            // TODO open the channel activity
        });
        binding.channelList.setOnUserClickListener(theUser -> {
            // open your user profile
        });
    }
}
package com.example.chattutorial

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.example.chattutorial.databinding.ActivityMainBinding
import com.getstream.sdk.chat.Chat
import com.getstream.sdk.chat.viewmodel.ChannelListViewModel
import io.getstream.chat.android.client.errors.ChatError
import io.getstream.chat.android.client.logger.ChatLogLevel
import io.getstream.chat.android.client.models.Filters.`in`
import io.getstream.chat.android.client.models.Filters.and
import io.getstream.chat.android.client.models.Filters.eq
import io.getstream.chat.android.client.models.User
import io.getstream.chat.android.client.socket.InitConnectionListener

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        // setup the client using the example API key
        // normally you would call init in your Application class and not the activity
        val chat = Chat.Builder("b67pax5b2wdq", this.applicationContext)
            .logLevel(ChatLogLevel.ALL)
            .build()

        val client = chat.client

        val user = User("summer-brook-2")
        user.extraData["name"] = "Paranoid Android"
        user.extraData["image"] = "https://bit.ly/2TIt8NR"
        // User token is typically provided by your server when the user authenticates
        val token =
            "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VtbWVyLWJyb29rLTIifQ.CzyOx8kgrc61qVbzWvhV1WD3KPEo5ZFZH-326hIdKz0"
        client.setUser(user, token, object : InitConnectionListener() {
            override fun onSuccess(data: ConnectionData) {
                Log.i("MainActivity", "setUser completed")
            }

            override fun onError(error: ChatError) {
                Toast.makeText(applicationContext, error.toString(), Toast.LENGTH_LONG).show()
                Log.e("MainActivity", "setUser onError")
            }
        })

        // we're using data binding in this example
        val binding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)
        // Specify the current activity as the lifecycle owner.
        binding.lifecycleOwner = this

        // most the business logic for chat is handled in the ChannelListViewModel view model
        val viewModel = ViewModelProvider(this).get(ChannelListViewModel::class.java)

        binding.viewModel = viewModel
        binding.channelList.setViewModel(viewModel, this)

        // query all channels of type messaging
        val filter = and(eq("type", "messaging"), `in`("members", listOf(user.id)))
        viewModel.setChannelFilter(filter)

        // click handlers for clicking a user avatar or channel
        binding.channelList.setOnChannelClickListener { channel ->
            // open the channel activity
        }
        binding.channelList.setOnUserClickListener { user ->
            // open your user profile
        }
    }
}

A Working Messaging Channel List

Run your application and you should see a channel list interface as shown on the right. Let's have a quick look at the source code shown above. First, we start by creating a connection to Stream by initializing StreamChat. For a production app, we recommend configuring StreamChat in your Application class.

Next we call setUser on the client object. This authenticates the current user. Because this is a tutorial, we pre-generated a user token to keep things easy to follow. In a real-world application, your authentication backend would generate such token at log-in / signup and hand it over to the mobile app. More information about this is available on the Chat API docs.

Most of the logic for this activity is done in the viewModel. The viewModel uses the low-level client to keep all chat information in sync and exposes it to the activity and layout via LiveData. The other thing to note is the FilterObject. This demo shows all the channels of the "messaging" type. Stream supports advanced queries to retrieve a list of channels. The most common use case is querying the channels that a certain member is participating in. The documentation about querying channels covers this in more detail.

Channel List Chat interface for Java / Kotlin on Android

Message List

As a next step let's open up one of these channels and start sending messages. To do this we'll leverage the ChannelHeader, MessageList, and MessageInput custom views. Again, these custom views are just a starting point. You can always use the lower level API to build your views from scratch.

Create new empty activity (File > New > Activity > Empty Activity) and name it ChannelActivity.

Next, open up activity_channel.xml and update the layout to match this example:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.getstream.sdk.chat.viewmodel.ChannelViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.example.chattutorial.ChannelActivity">

        <com.getstream.sdk.chat.view.ChannelHeaderView
            android:id="@+id/channelHeader"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#FFF"
            app:streamChannelHeaderBackButtonShow="true"
            app:layout_constraintEnd_toStartOf="@+id/messageList"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.getstream.sdk.chat.view.MessageListView
            android:id="@+id/messageList"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginBottom="10dp"
            android:background="#FFF"
            app:layout_constraintBottom_toTopOf="@+id/message_input"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/channelHeader"
            />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:padding="6dp"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:isGone="@{!safeUnbox(viewModel.loading)}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ProgressBar
            android:layout_width="25dp"
            android:layout_height="25dp"
            app:isGone="@{!safeUnbox(viewModel.loadingMore)}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="parent" />

        <com.getstream.sdk.chat.view.MessageInputView
            android:id="@+id/message_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:layout_marginBottom="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintStart_toEndOf="@+id/messageList" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Note how the constraint layout contains a ChannelHeaderView, MessageListView, MessageInput and 2 loading indicators. As a next step we'll want to launch the ChannelActivity when you click a channel. To do this open MainActivity and make the following changes:


// Setup two strings for the intent
public class MainActivity extends AppCompatActivity {

+    public static final String EXTRA_CHANNEL_TYPE = "com.example.chattutorial.CHANNEL_TYPE";
+    public static final String EXTRA_CHANNEL_ID = "com.example.chattutorial.CHANNEL_ID";

    //....

    // replace the channel click handler
     binding.channelList.setOnChannelClickListener(channel -> {
            // open the channel activity
+            Intent intent = new Intent(MainActivity.this, ChannelActivity.class);
+            intent.putExtra(EXTRA_CHANNEL_TYPE, channel.getType());
+            intent.putExtra(EXTRA_CHANNEL_ID, channel.getId());
+            startActivity(intent);
     });

}

class MainActivity : AppCompatActivity() {

    //....

    // replace the channel click handler
        binding.channelList.setOnChannelClickListener { channel ->
            // open the channel activity
+            val intent = ChannelActivity.newIntent(this, channel)
+            startActivity(intent)
        }

    //....

}

Now all that's left to do is to configure the Channel Activity. Replace the source code in ChannelActivity with:

package com.example.chattutorial;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;

import com.example.chattutorial.databinding.ActivityChannelBinding;
import com.getstream.sdk.chat.utils.PermissionChecker;
import com.getstream.sdk.chat.view.MessageInputView;
import com.getstream.sdk.chat.viewmodel.ChannelViewModel;
import com.getstream.sdk.chat.viewmodel.ChannelViewModelFactory;

import java.util.ArrayList;
import java.util.List;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import io.getstream.chat.android.client.ChatClient;
import io.getstream.chat.android.client.controllers.ChannelController;
import io.getstream.chat.android.client.events.ChatEvent;
import io.getstream.chat.android.client.events.TypingStartEvent;
import io.getstream.chat.android.client.events.TypingStopEvent;
import io.getstream.chat.android.client.models.Channel;
import io.getstream.chat.android.client.models.User;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;

public class ChannelActivity extends AppCompatActivity
        implements MessageInputView.PermissionRequestListener {

    private ChannelViewModel viewModel;
    private ActivityChannelBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // receive the intent and create a channel object
        Intent intent = getIntent();
        String channelType = intent.getStringExtra(MainActivity.EXTRA_CHANNEL_TYPE);
        String channelId = intent.getStringExtra(MainActivity.EXTRA_CHANNEL_ID);
        ChatClient client = ChatClient.instance();
        ChannelController channel = client.channel(channelType, channelId);

        // we're using data binding in this example
        binding = DataBindingUtil.setContentView(this, R.layout.activity_channel);
        // most the business logic of the chat is handled in the ChannelViewModel view model
        binding.setLifecycleOwner(this);

        initViewModel(channelType, channelId);
    }

    private void initViewModel(String channelType, String channelId) {

        ChannelViewModelFactory viewModelFactory = new ChannelViewModelFactory(this.getApplication(), channelType, channelId);
        viewModel = new ViewModelProvider(this, viewModelFactory).get(ChannelViewModel.class);
        ChatClient client = ChatClient.instance();
        ChannelController channelController = client.channel(channelType, channelId);
        LifecycleOwner lifecycleOwner = this;

        viewModel.getInitialized().observe(this, channel -> {

            // connect the view model
            binding.setViewModel(viewModel);

            binding.channelHeader.setViewModel(viewModel, this)
            binding.messageList.setViewModel(viewModel, lifecycleOwner);
            binding.messageInput.setViewModel(viewModel, lifecycleOwner);
        });
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // If you are using own MessageInputView please comment this line.
        binding.messageInput.captureMedia(requestCode, resultCode, data);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        // If you are using own MessageInputView please comment this line.
        binding.messageInput.permissionResult(requestCode, permissions, grantResults);
    }

    @Override
    public void openPermissionRequest() {
        PermissionChecker.permissionCheck(this, null);
    }
}
package com.example.chattutorial

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.chattutorial.databinding.ActivityChannelBinding
import com.getstream.sdk.chat.utils.PermissionChecker
import com.getstream.sdk.chat.view.MessageInputView.PermissionRequestListener
import com.getstream.sdk.chat.viewmodel.ChannelViewModel
import com.getstream.sdk.chat.viewmodel.ChannelViewModelFactory
import io.getstream.chat.android.client.ChatClient
import io.getstream.chat.android.client.events.TypingStartEvent
import io.getstream.chat.android.client.events.TypingStopEvent
import io.getstream.chat.android.client.models.Channel
import java.util.*

class ChannelActivity : AppCompatActivity(), PermissionRequestListener {

    private lateinit var binding: ActivityChannelBinding


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // receive the intent and create a channel object
        val intent = intent
        val channelType = intent.getStringExtra(EXTRA_CHANNEL_TYPE)
        val channelId = intent.getStringExtra(EXTRA_CHANNEL_ID)

        // we're using data binding in this example
        binding = DataBindingUtil.setContentView(this, R.layout.activity_channel)
        // most the business logic of the chat is handled in the ChannelViewModel view model
        binding.lifecycleOwner = this

        initViewModel(channelType, channelId)
    }

    override fun onActivityResult(
        requestCode: Int,
        resultCode: Int,
        data: Intent?
    ) {
        super.onActivityResult(requestCode, resultCode, data)
        // If you are using own MessageInputView please comment this line.
        binding.messageInput.captureMedia(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String?>,
        grantResults: IntArray
    ) { // If you are using own MessageInputView please comment this line.
        binding.messageInput.permissionResult(requestCode, permissions, grantResults)
    }

    override fun openPermissionRequest() {
        PermissionChecker.permissionCheck(this, null)
        // If you are writing a Channel Screen in a Fragment, use the code below instead of the code above.
        // PermissionChecker.permissionCheck(getActivity(), this);
    }

    private fun initViewModel(
        channelType: String,
        channelId: String
    ) {

        val viewModelFactory = ChannelViewModelFactory(application, channelType, channelId)
        val viewModel = ViewModelProvider(this, viewModelFactory).get(ChannelViewModel::class.java)
        val client = ChatClient.instance()
        val channelController = client.channel(channelType, channelId)
        val lifeCycleOwner = this

        viewModel.initialized.observe(this, Observer<Channel> { channel ->
            // connect the view model
            binding.viewModel = viewModel
            binding.messageList.setViewModel(viewModel, this)
            binding.messageInput.setViewModel(viewModel, this)
            binding.channelHeader.setViewModel(viewModel, this)
            // If you are using own MessageInputView please comment this line.
            binding.messageInput.setPermissionRequestListener(this)
        }
        )
    }


    companion object {

        private const val EXTRA_CHANNEL_TYPE = "com.example.chattutorial.CHANNEL_TYPE"
        private const val EXTRA_CHANNEL_ID = "com.example.chattutorial.CHANNEL_ID"

        fun newIntent(context: Context, channel: Channel): Intent {
            val intent = Intent(context, ChannelActivity::class.java)
            intent.putExtra(EXTRA_CHANNEL_TYPE, channel.type)
            intent.putExtra(EXTRA_CHANNEL_ID, channel.id)
            return intent
        }
    }
}

There are a few things to talk about here. Similar to the MainActivity most of the logic is handled by the viewModel. You'll see how we pass the viewModel to the various components. If you hit run and click a channel you should see a chat interface as shown on the right.

Chat Features

This is a good moment to talk about features. Did you already try posting a link? Stream Chat provides you with all the features you need to build an engaging messaging experience:

  1. Link previews will be generated automatically when you send a link
  2. Commands: focus on the composer and type / to use commands, like /giphy
  3. Reactions: tap on a message to add reactions or long-press on images
  4. Attach images: use the plus button ⨁ in the composer to attach images or files
  5. Edit message: long-press on your messages to open the menu with the Edit button
  6. Typing events: you’ll see typing events at the bottom of messages when someone starts typing
  7. Threads: you can create threads to any message in a channel, just tap on a message and hit Reply to enter the thread view.

Some of the features are hard to see in action with just one user online. If you are interested, you can open the same channel on the web and try user to user interactions like typing events, reactions, and threads.

Custom Android chat bubble example with Java / Kotlin

Chat Message Customization

You now have a fully functional chat interface, not bad for 5 minutes of work. There are three ways to customize your chat experience:

  1. Style the MessageListView using attributes (easy)
  2. Write a view holder to render custom message or attachment types (easy)
  3. Build a view using Stream's low-level Java SDK (quite a bit of work)

In the next sections, we'll show an example for each type of customization. We'll start with changing the colors of the chat messages to match your theme.

Open activity_channel.xml and replace the MessageListView with the following code for an epic green message style:

<com.getstream.sdk.chat.view.MessageListView
    android:id="@+id/messageList"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginBottom="10dp"
    app:layout_constraintBottom_toTopOf="@+id/message_input"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/channelHeader"

    android:background="#f3f5f8"
    app:streamMessageTextColorMine="#FFF"
    app:streamMessageBackgroundColorMine="#70af74"
    app:streamMessageTextColorTheirs="#000"
    app:streamMessageBackgroundColorTheirs="#FFF"
    />

Next, update your app and write a new message. Your chat should now look similar to the image shown on the right. Note how messages that you wrote now show up with a green background and white text.

Message List Chat interface for Java / Kotlin on Android

A custom attachment type

Ok, let's try to make a more complicated change and implement an Attachment ViewHolder. You could use this approach to embed a shopping cart in your chat, share a location, or perhaps implement a poll. For this example, we'll keep it simple and customize the layout for images shared from Imgur. We're going to render the Imgur logo over images from the imgur.com domain.

As a first step we'll create a new Java or Kotlin file called MyMessageViewHolderFactory and enter the following code:

package com.example.chattutorial;

import android.view.ViewGroup;

import com.getstream.sdk.chat.adapter.AttachmentListItemAdapter;
import com.getstream.sdk.chat.adapter.BaseAttachmentViewHolder;
import com.getstream.sdk.chat.adapter.MessageViewHolderFactory;

import java.util.List;

import io.getstream.chat.android.client.models.Attachment;
import io.getstream.chat.android.client.models.Message;

public class MyMessageViewHolderFactory extends MessageViewHolderFactory {
    private static int IMGUR_TYPE = 0;

    @Override
    public int getAttachmentViewType(Message message, Boolean mine, Position position, List<Attachment> attachments, Attachment attachment) {
        if (attachment.getImageUrl() != null && attachment.getImageUrl().contains("imgur")) {
            return IMGUR_TYPE;
        }
        return super.getAttachmentViewType(message, mine, position, attachments, attachment);
    }

    @Override
    public BaseAttachmentViewHolder createAttachmentViewHolder(AttachmentListItemAdapter adapter, ViewGroup parent, int viewType) {
        BaseAttachmentViewHolder holder;
        if (viewType == IMGUR_TYPE) {
            holder = new AttachmentViewHolderImgur(R.layout.list_item_attach_imgur, parent);
        } else {
            holder = super.createAttachmentViewHolder(adapter, parent, viewType);
        }

        return holder;
    }
}
package com.example.chattutorial

import android.view.ViewGroup

import com.getstream.sdk.chat.adapter.AttachmentListItemAdapter
import com.getstream.sdk.chat.adapter.BaseAttachmentViewHolder
import com.getstream.sdk.chat.adapter.MessageViewHolderFactory
import io.getstream.chat.android.client.models.Attachment
import io.getstream.chat.android.client.models.Message

class MyMessageViewHolderFactory : MessageViewHolderFactory() {

    override fun getAttachmentViewType(
        message: Message?,
        mine: Boolean?,
        position: Position?,
        attachments: List<Attachment>?,
        attachment: Attachment
    ): Int {
        val imageUrl = attachment.imageUrl ?: ""
        return if (imageUrl.indexOf("imgur") != -1) {
            IMGUR_TYPE
        } else super.getAttachmentViewType(message, mine, position, attachments, attachment)
    }

    override fun createAttachmentViewHolder(
        adapter: AttachmentListItemAdapter,
        parent: ViewGroup,
        viewType: Int
    ): BaseAttachmentViewHolder {
        val holder: BaseAttachmentViewHolder
        if (viewType == IMGUR_TYPE) {
            holder = AttachmentViewHolderImgur(R.layout.list_item_attach_imgur, parent)
        } else {
            holder = super.createAttachmentViewHolder(adapter, parent, viewType)
        }

        return holder
    }

    companion object {
        private const val IMGUR_TYPE = 0
    }
}

As a next step we'll hookup your new Message View Holder factory to the message list component. Open ChannelActivity and add this line just below the // connect the view model line:

   // connect the view model
+   binding.messageList.setViewHolderFactory(new MyMessageViewHolderFactory());
   // connect the view model
+   val factory = MyMessageViewHolderFactory()
+   binding.messageList.setViewHolderFactory(factory)

The MessageList now knows about your custom view holder factory. We now need to implement the AttachmentViewHolderImgur class. Create a new file called AttachmentViewHolderImgur and add this code:

package com.example.chattutorial;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.getstream.sdk.chat.adapter.BaseAttachmentViewHolder;
import com.getstream.sdk.chat.adapter.MessageListItem;
import com.getstream.sdk.chat.utils.roundedImageView.PorterShapeImageView;
import com.getstream.sdk.chat.view.MessageListView;
import com.bumptech.glide.Glide;
import com.getstream.sdk.chat.view.MessageListViewStyle;


public class AttachmentViewHolderImgur extends BaseAttachmentViewHolder {
    private PorterShapeImageView iv_media_thumb;

    AttachmentViewHolderImgur(int resId, ViewGroup parent) {
        super(resId, parent);

        iv_media_thumb = itemView.findViewById(R.id.iv_media_thumb);
    }

    @Override
    public void bind(@NonNull Context context, @NonNull MessageListItem messageListItem, @NonNull io.getstream.chat.android.client.models.Message message, @NonNull io.getstream.chat.android.client.models.Attachment attachment, @NonNull MessageListViewStyle style, @NonNull MessageListView.BubbleHelper bubbleHelper, @Nullable MessageListView.AttachmentClickListener clickListener, @Nullable MessageListView.MessageLongClickListener longClickListener) {
        Drawable background = bubbleHelper.getDrawableForAttachment(messageListItem.getMessage(), messageListItem.isMine(), messageListItem.getPositions(), attachment);
        iv_media_thumb.setShape(context, background);

        Glide.with(context)
                .load(attachment.getThumbUrl())
                .into(iv_media_thumb);
    }

}
package com.example.chattutorial

import android.content.Context
import android.view.ViewGroup
import com.bumptech.glide.Glide
import com.getstream.sdk.chat.adapter.BaseAttachmentViewHolder
import com.getstream.sdk.chat.adapter.MessageListItem
import com.getstream.sdk.chat.utils.roundedImageView.PorterShapeImageView
import com.getstream.sdk.chat.view.MessageListView
import com.getstream.sdk.chat.view.MessageListViewStyle


class AttachmentViewHolderImgur(resId: Int, parent: ViewGroup) :
    BaseAttachmentViewHolder(resId, parent) {
    private val ivMediaThumb: PorterShapeImageView = itemView.findViewById(R.id.iv_media_thumb)

    override fun bind(
        context: Context,
        messageListItem: MessageListItem,
        message: io.getstream.chat.android.client.models.Message,
        attachment: io.getstream.chat.android.client.models.Attachment,
        style: MessageListViewStyle,
        bubbleHelper: MessageListView.BubbleHelper,
        clickListener: MessageListView.AttachmentClickListener?,
        longClickListener: MessageListView.MessageLongClickListener?
    ) {
        val background = bubbleHelper.getDrawableForAttachment(
            messageListItem.message,
            messageListItem.isMine,
            messageListItem.positions,
            attachment
        )
        ivMediaThumb.setShape(context, background)

        Glide.with(context)
            .load(attachment.thumbUrl)
            .into(ivMediaThumb)
    }
}

Next download the Imgur logo and add it to your drawable folder.


As a final step create a XML Layout called list_item_attach_imgur (File > New > XML > Layout XML File):

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.getstream.sdk.chat.utils.roundedImageView.PorterShapeImageView
        android:id="@+id/iv_media_thumb"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginStart="1px"
        android:layout_marginTop="1px"
        android:layout_marginEnd="1px"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_logo"
        android:layout_width="150dp"
        android:layout_height="50dp"
        android:background="@drawable/imgur_logo"
        app:layout_constraintBottom_toBottomOf="@+id/iv_media_thumb"
        app:layout_constraintEnd_toEndOf="@+id/iv_media_thumb"
        app:layout_constraintStart_toStartOf="@+id/iv_media_thumb"
        app:layout_constraintTop_toTopOf="@+id/iv_media_thumb" />

</androidx.constraintlayout.widget.ConstraintLayout>

Your custom view holder

Run your app and you should now see the Imgur logo displayed over images from Imgur. You can test this by posting an Imgur link like this one: https://imgur.com/gallery/CiZeA

This was, of course, a very simple change. But you could use the same approach to implement a product preview, shopping cart, location sharing or a poll. Note you can typically achieve what you want by using a custom attachment type. Most of the time you don't need a custom message type (which is more complex to implement).

Imgur Logo overlay on the in-app messaging chat interface

Creating your own Channel Header component

The low-level Java SDK client enables you to talk directly to Stream's API. This gives you the flexibility to implement any messaging/chat UI that you want. As an example, we'll build a channel header UI element that shows who is typing.

First off, open activity_channel.xml and add a typing variable. Note that you need to add the typing variable in the data section of your layout:

<data>

    <variable
        name="viewModel"
        type="com.getstream.sdk.chat.viewmodel.ChannelViewModel" />

+    <variable
+        name="typing"
+        type="String" />
</data>

Next in activity_channel.xml replace the com.getstream.sdk.chat.view.ChannelHeaderView with a TextView:

-   <com.getstream.sdk.chat.view.ChannelHeaderView
-    android:id="@+id/channelHeader"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    app:layout_constraintStart_toStartOf="parent"
-    app:layout_constraintTop_toTopOf="parent"
-    app:streamChannelHeaderBackButtonShow="true" />

+   <TextView
+    android:id="@+id/channelHeader"
+    android:layout_width="match_parent"
+    android:layout_height="31dp"
+    android:background="#CCC"
+    android:text="@{typing}"
+    android:gravity="center"
+    android:layout_marginBottom="5dp"
+    app:layout_constraintEnd_toStartOf="@+id/messageList"
+    app:layout_constraintStart_toStartOf="parent"
+    app:layout_constraintTop_toTopOf="parent" />

Now open ChannelActivity and attach an event handler to the channel. The complete documentation for the Java client is available in the docs. We use the event handler to populate a LiveData object. Next, we observe the LiveData object and write to the layout's typing variable. Remove the channelHeader.setViewModel call and make the following changes to the code:


// inside onCreate

-    binding.channelHeader.setViewModel(viewModel, this);

+    MutableLiveData<List<String>> currentlyTyping = new MutableLiveData<>(new ArrayList<>());
+    channelController.events().subscribe(new Function1<ChatEvent, Unit>() {
+        @Override
+        public Unit invoke(ChatEvent event) {
+            User user = event.getUser();
+            String name = (String) user.getExtraData().get("name");
+    
+            if (event instanceof TypingStartEvent) {
+                List<String> typingCopy = currentlyTyping.getValue();
+                if (!typingCopy.contains(name)) {
+                    typingCopy.add(name);
+                }
+                currentlyTyping.postValue(typingCopy);
+            } else if (event instanceof TypingStopEvent) {
+                List<String> typingCopy = currentlyTyping.getValue();
+                typingCopy.remove(name);
+                currentlyTyping.postValue(typingCopy);
+            }
+            return null;
+        }
+    });
+    currentlyTyping.observe(lifecycleOwner, users -> {
+        String typing = "nobody is typing";
+        if (!users.isEmpty()) {
+            typing = "typing: " + TextUtils.join(", ", users);
+        }
+        binding.setTyping(typing);
+    });

// inside onCreate
  
-    // TODO: add typing events
-    binding.channelHeader.setViewModel(viewModel, this)
  
+    val currentlyTyping = MutableLiveData<List<String>>(ArrayList())
+    
+    channelController.events().subscribe {
+        val name = it.user!!.extraData["name"]!! as String
+        val typing = currentlyTyping.value ?: listOf()
+        val typingCopy: MutableList<String> = typing.toMutableList()
+        when (it) {
+            is TypingStartEvent -> {
+                if (typingCopy.contains(name).not()) {
+                    typingCopy.add(name)
+                }
+                currentlyTyping.postValue(typingCopy)
+            }
+            is TypingStopEvent -> {
+                typingCopy.remove(name)
+                currentlyTyping.postValue(typingCopy)
+            }
+        }
+    }
    
+    val typingObserver = Observer<List<String>> { users ->
+        var typing = "nobody is typing"
+        if (users.isNotEmpty()) {
+            typing = "typing: " + users.joinToString(", ")
+        }
+        binding.typing = typing
+    }
+    currentlyTyping.observe(lifeCycleOwner, typingObserver)
  
// ...

Your custom channel view

When you run your chat app you should now see a little typing indicator above the channel. Note that the ChannelViewModel also exposes the getTypingUsers method which returns a LiveData object. The above example was a little complex, but it gives you an idea of how you can use the lower-level API.

In-app example of typing into an Android messaging window

Final thoughts

In this Android Chat App tutorial, we learned how to build a fully functional chat in-app using Java or Kotlin. We also showed how easy it is to customize the behavior and build any type of chat or messaging experience.

The underlying chat API is based on Go, RocksDB, and Raft. This makes the chat experience extremely fast with response times that are often below 10ms. Stream powers activity feeds and chat for over 500 million end-users, so you can feel confident about our APIs.

Both the Java Chat SDK and the Chat API have plenty more features available to support more advanced use-cases such as push notifications, content moderation, rich messages and more. Feel free to contact us if you have any questions.

Next Steps

In addition to this tutorial you might also want to learn about the Stream Chat API, the iOS Swift Chat SDK, Python Chat, React Chat and React Native Chat, along with our free Chat UI Kit for more resources on building your app with Stream.