TUTORIAL

How to Build an Android Chat App with Java or Kotlin

Learn how to use our Android Chat SDK to create a polished 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 chat messaging for your Android app.

Android Chat SDK Setup: Java or Kotlin

You can use the Android Chat SDK with both Java or Kotlin, this tutorial has code examples in both languages.

Add Stream Chat to Your Application Example

In order to follow this messages app 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 using the Empty Activity template and name it ChatTutorial. Pick either Java or Kotlin, then enable "Use androidx.* artifacts" and target API level 28. Note that the chat SDK is compatible with API level 21 or higher, for the purpose of this tutorial we'll use API level 28.

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 {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.2'
    }
}

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'

android {
    compileSdkVersion 28
    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:3.3.0'
+    implementation 'androidx.recyclerview:recyclerview:1.0.0'
+    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
+    implementation 'com.github.bumptech.glide:glide:4.9.0'


    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

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 both a low-level Java client and custom views to help you quickly build your chat interface. You can customize the custom views, and if you need more flexibility you can always fall back to the low-level client.

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.ViewModelProviders;

import com.example.chattutorial.databinding.ActivityMainBinding;
import com.getstream.sdk.chat.StreamChat;
import com.getstream.sdk.chat.enums.FilterObject;
import com.getstream.sdk.chat.rest.User;
import com.getstream.sdk.chat.rest.core.Client;
import com.getstream.sdk.chat.viewmodel.ChannelListViewModel;

import java.util.HashMap;

import static com.getstream.sdk.chat.enums.Filters.and;
import static com.getstream.sdk.chat.enums.Filters.in;
import static com.getstream.sdk.chat.enums.Filters.eq;


/**
 * This activity shows a list of channels
 */
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        // setup the client using the example API key
        // normal you would call init in your Application class and not the activity
        StreamChat.init("b67pax5b2wdq", this.getApplicationContext());
        Client client = StreamChat.getInstance(this.getApplication());
        HashMap<String, Object> extraData = new HashMap<>();
        extraData.put("name", "Paranoid Android");
        extraData.put("image", "https://bit.ly/2TIt8NR");
        User currentUser = new User("restless-silence-2", extraData);
        // User token is typically provided by your server when the user authenticates
        client.setUser(currentUser, "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoicmVzdGxlc3Mtc2lsZW5jZS0yIn0.6vbCknCNEDQWmPPQlVRc2Hdguqt9DiTwHx-mzpUl3X0");

        // 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 = ViewModelProviders.of(this).get(ChannelListViewModel.class);
        binding.setViewModel(viewModel);
        binding.channelList.setViewModel(viewModel, this);

        // query all channels of type messaging
        FilterObject filter = and(eq("type", "messaging"), in("members", "restless-silence-2"));
        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
        });

    }
}
package com.example.chattutorial

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProviders
import com.example.chattutorial.databinding.ActivityMainBinding
import com.getstream.sdk.chat.StreamChat
import com.getstream.sdk.chat.enums.Filters.*
import com.getstream.sdk.chat.rest.User
import com.getstream.sdk.chat.viewmodel.ChannelListViewModel
import java.util.*


/**
 * This activity shows a list of channels
 */
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
        StreamChat.init("b67pax5b2wdq", this.applicationContext)
        val client = StreamChat.getInstance(this.application)
        val extraData = HashMap<String, Any>()
        extraData["name"] = "Paranoid Android"
        extraData["image"] = "https://bit.ly/2TIt8NR"
        val currentUser = User("restless-silence-2", extraData)
        // User token is typically provided by your server when the user authenticates
        client.setUser(
            currentUser,
            "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoicmVzdGxlc3Mtc2lsZW5jZS0yIn0.6vbCknCNEDQWmPPQlVRc2Hdguqt9DiTwHx-mzpUl3X0"
        )

        // 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 = ViewModelProviders.of(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", "restless-silence-2"))
        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 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:

// add an import for Intent
+import android.content.Intent;
// 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 androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;

import com.example.chattutorial.databinding.ActivityChannelBinding;
import com.getstream.sdk.chat.StreamChat;
import com.getstream.sdk.chat.model.Channel;
import com.getstream.sdk.chat.rest.core.Client;
import com.getstream.sdk.chat.view.MessageInputView;
import com.getstream.sdk.chat.viewmodel.ChannelViewModel;
import com.getstream.sdk.chat.viewmodel.ChannelViewModelFactory;

/**
 * Show the messages for a channel
 */
public class ChannelActivity extends AppCompatActivity
        implements MessageInputView.OpenCameraViewListener {

    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);
        Client client = StreamChat.getInstance(getApplication());

        // 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);

        Channel channel = client.channel(channelType, channelID);
        viewModel = ViewModelProviders.of(this,
                new ChannelViewModelFactory(this.getApplication(), channel)
        ).get(ChannelViewModel.class);

        // set listeners
        binding.messageInput.setOpenCameraViewListener(this);

        // connect the view model
        binding.setViewModel(viewModel);
        binding.channelHeader.setViewModel(viewModel, this);
        binding.messageList.setViewModel(viewModel, this);
        binding.messageInput.setViewModel(viewModel, this);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        binding.messageInput.captureMedia(requestCode, resultCode, data);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        binding.messageInput.permissionResult(requestCode, permissions, grantResults);
    }

    @Override
    public void openCameraView(Intent intent, int REQUEST_CODE) {
        startActivityForResult(intent, REQUEST_CODE);
    }
}
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.ViewModelProviders
import com.example.chattutorial.databinding.ActivityChannelBinding
import com.getstream.sdk.chat.StreamChat
import com.getstream.sdk.chat.model.Channel
import com.getstream.sdk.chat.view.MessageInputView
import com.getstream.sdk.chat.viewmodel.ChannelViewModel
import com.getstream.sdk.chat.viewmodel.ChannelViewModelFactory

/**
 * Show the messages for a channel
 */
class ChannelActivity : AppCompatActivity(),
    MessageInputView.OpenCameraViewListener {

    private var viewModel: ChannelViewModel? = null
    private var binding: ActivityChannelBinding? = null

    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)
        val client = StreamChat.getInstance(application)

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

        var channel = client.channel(channelType, channelID)
        viewModel = ViewModelProviders.of(
            this,
            ChannelViewModelFactory(this.application, channel)
        ).get(ChannelViewModel::class.java)

        // set listeners
        binding!!.messageInput.setOpenCameraViewListener(this)

        // connect the view model
        binding!!.viewModel = viewModel
        binding!!.messageList.setViewModel(viewModel!!, this)
        binding!!.messageInput.setViewModel(viewModel, this)
        binding!!.channelHeader.setViewModel(viewModel, this)
    }

    public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        binding!!.messageInput.captureMedia(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>,
        grantResults: IntArray
    ) {
        binding!!.messageInput.permissionResult(requestCode, permissions, grantResults)
    }

    override fun openCameraView(intent: Intent, REQUEST_CODE: Int) {
        startActivityForResult(intent, REQUEST_CODE)
    }

    companion object {

        private val EXTRA_CHANNEL_TYPE = "com.example.chattutorial.CHANNEL_TYPE"
        private 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.BaseAttachmentViewHolder;
import com.getstream.sdk.chat.adapter.AttachmentListItemAdapter;
import com.getstream.sdk.chat.adapter.MessageViewHolderFactory;
import com.getstream.sdk.chat.model.Attachment;
import com.getstream.sdk.chat.rest.Message;

import java.util.List;

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().indexOf("imgur") != -1) {
            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.BaseAttachmentViewHolder;
import com.getstream.sdk.chat.adapter.AttachmentListItemAdapter
import com.getstream.sdk.chat.adapter.MessageViewHolderFactory
import com.getstream.sdk.chat.model.Attachment
import com.getstream.sdk.chat.rest.Message

class MyMessageViewHolderFactory : MessageViewHolderFactory() {

    override fun getAttachmentViewType(
        message: Message?,
        mine: Boolean?,
        position: Position?,
        attachments: List<Attachment>?,
        attachment: Attachment
    ): Int {
        return if (attachment.imageURL != null && attachment.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 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 binding.setViewModel(viewModel) line:

binding.setViewModel(viewModel);
+   binding.messageList.setViewHolderFactory(new MyMessageViewHolderFactory());
// connect the view model
    binding!!.viewModel = viewModel
+   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 com.getstream.sdk.chat.adapter.BaseAttachmentViewHolder;
import com.getstream.sdk.chat.adapter.MessageListItem;
import com.getstream.sdk.chat.model.Attachment;
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;

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

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

    @Override
    public void bind(Context context,
                     MessageListItem messageListItem,
                     Attachment attachment,
                     MessageListViewStyle style,
                     MessageListView.AttachmentClickListener clickListener,
                     MessageListView.MessageLongClickListener longClickListener) {
        super.bind(context, messageListItem, attachment,style, clickListener, longClickListener);

        Drawable background = getBubbleHelper().getDrawableForAttachment(messageListItem.getMessage(), messageListItem.isMine(), messageListItem.getPositions(), attachment);
        iv_media_thumb.setShape(context, background);
        iv_media_thumb.setOnClickListener(this);
        iv_media_thumb.setOnLongClickListener(this);

        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.model.Attachment
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 iv_media_thumb: PorterShapeImageView

    init {

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

    override fun bind(
        context: Context,
        messageListItem: MessageListItem,
        attachment: Attachment,
        style: MessageListViewStyle,
        clickListener: MessageListView.AttachmentClickListener,
        longClickListener: MessageListView.MessageLongClickListener
    ) {
        super.bind(context, messageListItem, attachment, style, clickListener, longClickListener)

        val background = bubbleHelper.getDrawableForAttachment(
            messageListItem.message,
            messageListItem.isMine,
            messageListItem.positions,
            attachment
        )
        iv_media_thumb.setShape(context, background)
        iv_media_thumb.setOnClickListener(this)
        iv_media_thumb.setOnLongClickListener(this)

        Glide.with(context)
            .load(attachment.thumbURL)
            .into(iv_media_thumb)
    }
}

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


As a final step create a layout called list_item_attach_imgur:

<?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/t/aww/Yf8E0pj

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

// new imports
+   import androidx.lifecycle.MutableLiveData;
+   import com.getstream.sdk.chat.model.Event;
+   import com.getstream.sdk.chat.rest.core.ChatChannelEventHandler;
+   import java.util.ArrayList;
+   import java.util.List;

// ...
// inside onCreate

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

+    MutableLiveData<List<String>> currentlyTyping = new MutableLiveData<>(new ArrayList<String>());
+    channel.addEventHandler(new ChatChannelEventHandler() {
+        @Override
+        public void onTypingStart(Event event) {
+            List<String> typingCopy = currentlyTyping.getValue();
+            if (!typingCopy.contains(event.getUser().getName())) {
+                typingCopy.add(event.getUser().getName());
+            }
+            currentlyTyping.postValue(typingCopy);
+        }
+
+        @Override
+        public void onTypingStop(Event event) {
+            List<String> typingCopy = currentlyTyping.getValue();
+            typingCopy.remove(event.getUser().getName());
+            currentlyTyping.postValue(typingCopy);
+        }
+    });
+    currentlyTyping.observe(this, users -> {
+        String typing = "nobody is typing";
+        if (!users.isEmpty()) {
+            typing = "typing: " + String.join(", ", users);
+        }
+        binding.setTyping(typing);
+    });
// new imports
+   import androidx.lifecycle.MutableLiveData
+   import com.getstream.sdk.chat.model.Event
+   import com.getstream.sdk.chat.rest.core.ChatChannelEventHandler
+   import java.util.ArrayList
+   import androidx.lifecycle.Observer

// ...
// inside onCreate

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

+   val currentlyTyping = MutableLiveData<List<String>>(ArrayList())
+   channel.addEventHandler(object : ChatChannelEventHandler() {
+       override fun onTypingStart(event: Event) {
+           val typingCopy : MutableList<String>? = currentlyTyping.value!!.toMutableList()
+           if (!typingCopy!!.contains(event.getUser().getName())) {
+               typingCopy.add(event.getUser().getName())
+           }
+           currentlyTyping.postValue(typingCopy)
+       }
+
+       override fun onTypingStop(event: Event) {
+           val typingCopy : MutableList<String>? = currentlyTyping.value!!.toMutableList()
+           typingCopy!!.remove(event.getUser().getName())
+           currentlyTyping.postValue(typingCopy)
+       }
+   })
+ 
+   val typingObserver = Observer<List<String>> { users ->
+       var typing: String = "nobody is typing"
+       if (!users.isEmpty()) {
+           typing = "typing: " + users.joinToString(", ")
+       }
+       binding!!.setTyping(typing)
+   }
+   currentlyTyping.observe(this,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.

Imgur Logo overlay on the chat interface

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.

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.