Building an Avengers Chat Application for Android (part 1)

10 min read

Android Devs Assemble! In part one of this 3-part tutorial, you’ll learn how to build an Avengers chat app with Kotlin and Jetpack just in time to save the day.

Jaewoong E.
Jaewoong E.
Published November 8, 2021 Updated March 10, 2022
Avengers Chat App - Part 1

To get started, you’ll learn how to build an Android Avengers messaging application using Kotlin, Coroutines, and Jetpack libraries like Hilt, Room, and Databinding.

The app will also implement a model-view-viewmodel (MVVM) architectural pattern. After reading this article, you will learn the entire architecture of this application, and how to build out the messaging features yourself.

Keep in mind this is part one of a three-part series. In later posts, you'll learn how to incorporate live chat, light and dark themes, color-coded themes based on your hero of choice, and much more.

For additional help, check out the links below:

Quick Run

If you’d like to see the final state of the app in action, clone the following repository from GitHub and build it on your Android device:

avengers_chat_clone
git clone https://github.com/skydoves/AvengersChat

Also, you can download the APK directly on the releases page.

The end result will look like this:


Understanding the Architecture

AvengersChat was built with Android Jetpack libraries like Lifecycle, Databinding, ViewModel, and Navigation. If you have a basic understanding of Jetpack libraries, building this application will be much easier.

If your project already uses Jetpack Compose, check out Announcing Stream Chat’s Jetpack Compose SDK to learn other ways to build applications with Stream Chat and Jetpack Compose.

As for design, AvengersChat incorporates dependency injection and repository patterns so that the project is loosely-coupled to each component.

This article won’t cover dependency injection and repository patterns in-depth, but if you’d like to learn more about them, see the resources below:.

Avengers Chat architecture diagram

Gradle Setup

Before getting started, import the Stream Android UI component SDK into your project. Add the below dependencies for your module + app level build.gradle file:

The latest Android version of the Chat SDK
groovy
repositories { 
    google() 
    mavenCentral() 
    maven { url "https://jitpack.io" } 
    jcenter() 
} 
 
dependencies { 
    // Stream Chat Android SDK
    implementation "io.getstream:stream-chat-android-ui-components:4.24.0" 
}

Note: To see the latest version of the SDK, check out Stream’s GitHub releases page.

Stream’s Chat SDK enables you to build any type of chat or messaging experience for Android. It consists of three major components:

  • Client: The client handles all API calls and receives events.
  • Offline: The offline library stores the data, implements optimistic UI updates, handles network failures, and exposes LiveData or StateFlow objects that make it easy to build your own UI on.
  • UI: The UI package includes ViewModels and custom views for common things like building a channel list, message list, a message input, and more.

Basically, the android-ui-components artifact contains the above three components at once. If you want to import them independently, check out Stream’s official Android Introduction.

ChatClient and ChatDomain

ChatClient and ChatDomain are some of the most frequently used main components in the Stream Chat SDK.

  • ChatClient is the main entry point for all low-level chat operations, like connecting and disconnecting users to the Stream server or sending, updating, and pinning messages.
  • ChatDomain is the main entry point for all LiveData and offline operations on chat for a currently connected user. For example, it handles the logic to get information about a currently connected user, querying available channel lists, querying users, and more.

Initializing ChatClient and ChatDomain in the Application Class

You can simply initialize ChatClient and ChatDomain in your Application class with the following commands:

kt
val chatClient = ChatClient.Builder(“uwg4ggudgjz9”, context)
    .logLevel(ChatLogLevel.ALL) // set to NOTHING in product
    .build()

ChatDomain
    .Builder(chatClient, context)
    .offlineEnabled() // enable offline support
    .build()

After initializing them, you can get their instances by invoking instance() methods in any other classes like below:

kt
val chatClient = ChatClient.instance()
val chatDomain = ChatDomain.instance()

If you want to observe real-time data changes for the currently connected user, you can get the LiveData<User?> from the ChatDomain:

kt
val user: LiveData<User?> = chatDomain.user

Initialize with Dependency Injection

If your project uses any Dependency Injection (DI) libraries like Dagger, Hilt, or Koin, check out the StreamModule, which is located at the di package of this project. AvengersChat uses Hilt for injecting dependencies.

The main role of the StreamModule is to provide singleton instances of ChatClient and ChatDomain so you can inject them into other classes. This way, you can keep your app loosely-coupled.

kt
@Module
@InstallIn(SingletonComponent::class)
object StreamModule {

    @Provides
    @Singleton
    fun provideStreamChatClient(
        @ApplicationContext context: Context
    ): ChatClient {
        val chatClient: ChatClient =
            ChatClient.Builder("uwg4ggudgjz9", context)
                .logLevel(ChatLogLevel.ALL)
                .build()
        
        ChatDomain

API Key

As you may have noticed already, you need a Stream API Key to initialize the ChatClient. If you want to build your own project, go on to the next step. But if you want to build for a quick, you can also use the developer API Key (uwg4ggudgjz9) instead.

Note: Never use a developer key in your production code!

Stream Integration

Stream supports a well-designed web dashboard to manage the users, channels, traffic, and more. You can get your API Key to build your own project by following the steps below:

  1. Go to Stream’s main page.
  2. Select Try For Free and create your account. (If you don’t have one already)
  3. Go to your Stream dashboard.
  4. Select Create App.
  5. Enter an App Name (like AvengersChat).
  6. Set your Feeds Server Location.
  7. Set the Environment to Development.
  8. Select Create App.

Are you looking to integrate activity feeds or chat into your app? Qualifying startups can apply for a free Stream Maker Account—you’ll get access to Stream’s Chat and Feeds APIs to see if it’s the right fit for you!

Stream Chat dashboard

Congratulations! 🎉 Now, you can see your API Key on the dashboard. By using your own API Key, you can build your own configurations. If you click your app on the dashboard, you can manage all of your channels, users, and messages over the web page.

Stream Chat dashboard explorer dropdown

To view your app’s chat data:

  1. In your dashboard’s nav menu, select the Chat dropdown menu.
  2. Select Explorer.

From this page, you can:

  • View and manage chat messaging data.
  • See all user and message databases in real time.
  • Create channels and users manually.

Authenticate and Connect the User

Now, it’s time to authenticate and connect the user to the Stream server. You can create an instance of User and connect the user to the Stream server by using the connectUser method like below:

kt
val token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiaXJvbm1hbiJ9.U5f7gP1Fl_vJKNnu3v_vhFrwP-VjqTCdKYey71eYyIw"
val user = User( 
    id = "ironman", 
    extraData = mutableMapOf( 
        "name" to “ironman, 
        "image" to "https://bit.ly/2TIt8NR",
    ), 
)
client.connectUser( 
    user = user, 
    token = token, // or client.devToken(userId) for getting developer token
).enqueue { result -> 
    if (result.isSuccess) { 
        // Handle success 
    } else { 
        // Handler error 
    } 

Getting the User Token

If you want to connect the user to the Stream server, you need to generate a token and insert it into the connectUser method. Stream uses JSON Web Tokens (JWT) to authenticate chat users so they can log in.

Typically, you should generate tokens server-side by creating a Server Client, but that’s outside the scope of this article.

If you’re interested in learning more about generating tokens, see the official documentation for Tokens & Authentication.

For this tutorial, you can either generate a token client-side with a Developer Token or manually generate a token:

Generate a Developer Token

Looking back in Step 3, if you set the Environment as Development on your dashboard, you can get a Developer Token with the code below:

kt
val chatClient = ChatClient.instance()
chatClient.devToken(userId = "ironman")

Manually Generate Tokens

To manually generate a JWT token:

  1. Go to your Stream dashboard.
  2. Copy your Stream Secret.
  3. Go to the Manually Generating Tokens page and enter your Secret and User ID.

Connect the User Asynchronously

The connectUser method initializes ChatClient for a specific user with their user token. If the connection is successful, Stream will log the user into its server and maintain the login status until you call the disconnect()** method.

The connectUser method returns Call, which is an executable interface that you can execute using the enqueue method.

From the payload, you will get an instance of Result that includes your request’s status code and a request body.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Here is a basic example of connecting to the Stream server by using connectUser:

kt
client.connectUser( 
    user = user, 
    token = token,
).enqueue { result -> 
    if (result.isSuccess) { 
        // Handle success 
    } else { 
        // Handler error 
    } 

AvengersChat Examples

Let’s look at the AvengersChat examples. AvengersChat uses Hilt, repository patterns, and coroutines to connect the user. As you can see below, the HomeRepository class gets an instance of ChatClient via dependency injection by Hilt:

kt
class HomeRepository @Inject constructor(
    private val chatClient: ChatClient,
    private val avengersDao: AvengersDao,
    private val dispatcher: CoroutineDispatcher,
) {
    
    @WorkerThread
    fun connectUser(avenger: Avenger) = flow {
        val user = User(
            id = avenger.id,
            extraData = avenger.extraData
        )
        val result = chatClient.connectUser(user, avenger.token).await()
        result.onSuccessSuspend {
            emit(result.data())

You can execute connectUser in a coroutine scope or in a Flow by using the await() suspending method.

This will suspend the coroutine context until it receives the response from the Stream server to connect the user. After getting the result, you can handle the success and error cases and emit the data to Flow immediately like in the above example.

Pro Tips for Handling Result: One fancy way to handle the Result response is to use onSuccess and onError extensions like the below example:

kt
val result = chatClient.connectUser(user, avenger.token).await()
result.onSuccess { // you can use `onSuccessSuspend` instead if you need to run suspending functions.
    // Handle success
}.onError {
    // Handle error
}

ConnectionData

The connectUser method returns ConnectionData if the request succeeds. The ConnectionData contains the User property that has information of the currently connected user. You can use the User object to load custom views like AvaterView, which shows a user profile image and indicates online status.

AvengersChat Examples

In AvengersChat, HomeViewModel holds the instance of the the User instance, and they can be observed as DataBinding properties.

kt
class HomeViewModel @AssistedInject constructor(
    private val homeRepository: HomeRepository,
    @Assisted val avenger: Avenger,
) : BindingViewModel() {

    @get:Bindable
    val connectionData: ConnectionData?
        by homeRepository.connectUser(avenger).asBindingProperty(null)

The connectionData property collects data (ConnectionData) from the Flow (return type of the homeRepository.collectUser method), and the collected data can be observed by XML layouts.

If you want to learn more about Compose and XML-based views, check out Jetpack Compose vs. XML-based UI Components for Stream Chat.

AvengersChat uses an Android DataBinding library called Bindables. if you’d like to know more about DataBinding, see the links below:

Build a Channel List Screen

Did you successfully connect the user to the Stream server in the previous steps? Congratulations! 🎉 Now, you can dive into building a channel list screen.

Kakao Talk screenshot example

First, look at the fragment_channel_list.xml layout file.

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
	
    <io.getstream.chat.android.ui.channel.list.header.ChannelListHeaderView
        android:id="@+id/channelListHeaderView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:background="@color/colorPrimary"
        tools:layout_height="56dp" />

    <io.getstream.chat.android.ui.channel.list.ChannelListView

You can build a channel list screen with just the ChannelListHeaderView and ChannelListView components.

ChannelListHeaderView consists of a user avatar, title, and an action button. You can show the information of the current user by network connection state.

The ChannelListView displays the core part of the screen, which renders channels for the active user based on defined filters.

If you want to learn more about these components, see the official documents for Channel Components. And let’s see how to bind the ChannelListView to the Stream service.

Business Logic

Next, you’ll learn how to build the business logic and bind the layouts with it. The below example shows how to bind the layout with ViewModels on the ChannelListFragment.

kt
private val factory = ChannelListViewModelFactory()
private val channelListViewModel: ChannelListViewModel by lazy {
    factory.create(ChannelListViewModel::class.java)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    channelListViewModel.bindView(binding.channelListView, viewLifecycleOwner)
}

That’s it. How simple it is!

Set OnClickListeners

You can add listeners to handle actions for ChannelListHeaderView and ChannelListView when the user clicks them:

kt
channelListHeaderView.setOnUserAvatarClickListener {
    // Handle when the user avatar is clicked.
}

channelListHeaderView.setOnActionButtonClickListener {
    // Handle when the action button is clicked.
}

channelListView.setChannelItemClickListener { channel ->
    // Start a message list activity or navigate to a message list fragment. 
}

AvengersChat Examples

Check out ChannelListFragment in AvengersChat with the code below:

kt
private val streamChannelListUIComponent by streamChannelListComponent()

This is an advanced example to reduce initialization relevant codes by implementing a new class StreamChannelListUIComponent and a lazy extension.

kt
private val streamChannelListUIComponent by streamChannelListComponent()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    streamChannelListUIComponent.bindLayout(binding.root)
}

The code snippet above executes the following processes:

  1. The StreamChannelListUIComponent initializes the ChannelListViewModelFactory and ChannelListViewModel lazily.
  2. The ChannelListView will be bound to the ChannelListViewModel in the StreamChannelListUIComponent by invoking the bindLayout method on the Fragment’s onViewCreated method.

This approach reduces repetition in the codebase and improves reusability for similar screens.

Build a Message List Screen

Now, you will build a message list screen using the fragment_message_list layout in the example below:

Kakao Talk Avengers Chat screenshot example message
xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  <io.getstream.chat.android.ui.message.list.header.MessageListHeaderView
        android:id="@+id/messageListHeaderView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <io.getstream.chat.android.ui.message.list.MessageListView
        android:id="@+id/messageListView"
        android:layout_width="0dp"

You can see the layout consists of the following three components:

  • MessageListHeaderView: Consists of a user avatar and channel status; you can also show some messages by network connection state.
  • MessageListView: Shows a list of paginated messages with threads, replies, reactions, and deleted messages.
  • MessageInputView: Handles the message input, as well as attachments and message actions like editing and replying.

If you want to learn more about these UI components, see Stream’s official documents for Message Components.

Business Logic

Now, it’s time to build the business logic and bind it with the layouts. The below example shows how to bind the layout with ViewModels on the MessagelListFragment:

kt
// initializes ViewModels
private val args: MessageListFragmentArgs by navArgs()
private val factory = MessageListViewModelFactory(args.cid)
private val messageListHeaderViewModel: MessageListHeaderViewModel by viewModels { factory }
private val messageListViewModel: MessageListViewModel by viewModels { factory }
private val messageInputViewModel: MessageInputViewModel by viewModels { factory }

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    messageListHeaderViewModel.bindView(binding.messageListHeaderView, viewLifecycleOwner)
    messageListViewModel.bindView(binding.messageListView, viewLifecycleOwner)
    messageInputViewModel.bindView(binding.messageInputView, viewLifecycleOwner)
}

That’s it! You can build the perfect message list screen with a ridiculously small amount of code.

One of the things you need to look out for is the Channel ID. Generally, the message list screen is the next flow of the channel list screen. You can pass the information of the channel from the channel list screen via Intent extras data (Activity) or arguments (Fragment). The below example shows how to pass the channel id from the channel list screen to the message list screen:

kt
channelListView.setChannelItemClickListener { channel ->
    findNavController().navigate(
        ChannelListFragmentDirections.actionToFragmentMessageList(
            channel.cid, null
        )
    )
}

Then, you can get the Intent extras data or arguments in your message list Activity or Fragment.

AvengersChat Examples

Check out MessageListFragment in AvengersChat with the code below:

kt
private val streamMessageListComponent by streamMessageListComponent()

This is an advanced example to reduce initialization relevant code by creating a new class StreamMessageListUIComponent and a lazy extension:

kt
private val streamMessageListComponent by streamMessageListComponent()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    
        streamMessageListComponent.initIds(args.cid)
        streamMessageListComponent.bindLayout(binding.root)
}

The code snippet above executes the following processes:

  1. The StreamMessageListUIComponent initializes the MessageListViewModelFactory and relevant ViewModels lazily.
  2. Those ViewModels will be bound with all UI components (like MessageListView) by invoking the bindView method on the Fragment’s onViewCreated method.

This approach reduces repetition in the codebase and improves reusability.

Wrapping Up

This concludes part one of the tutorial on building the AvengersChat application using the Stream Chat SDK.

AvengersChat uses Kotlin, Coroutines, Hilt, Room, and a lot of other Jetpack libraries based on the model-view-viewmodel (MVVM) architectural pattern. You can use a similar approach to build any messaging application you want.

Remember that AvengersChat is an open-source repository, so anyone can contribute to improving code, design, architectures, or whatever. So, let’s build something awesome! 😎

Here are a few more links to help you with the tutorial:

In the next part of the tutorial, you will learn how to customize UI components and build a live stream example. Specifically, you will cover:

  • Customizing global styles for the channel and message list
  • Building a live stream chat
  • Implementing direct message dialog

If you’d like to stay up to date with Stream, follow us on Twitter @getstream_io for more great technical content. You can also reach the author @github_skydoves if you have any questions or feedback.

Happy coding!

decorative lines
Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->