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:
- The AvengersChat GitHub repository (anyone can contribute!)
- Stream’s Android Chat SDK for messaging with Kotlin
- Stream’s Android Chat messaging Tutorial
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:
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:.
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:
1234567891011repositories { 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:
12345678val 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:
12val 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
:
1val 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.
12345678910111213141516171819202122232425262728@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 .Builder(chatClient, context) .offlineEnabled() .build() return chatClient } @Provides @Singleton fun provideStreamChatDomain(): ChatDomain { return ChatDomain.instance() } }
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:
- Go to Stream’s main page.
- Select Try For Free and create your account. (If you don’t have one already)
- Go to your Stream dashboard.
- Select Create App.
- Enter an App Name (like AvengersChat).
- Set your Feeds Server Location.
- Set the Environment to Development.
- 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!
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.
To view your app’s chat data:
- In your dashboard’s nav menu, select the Chat dropdown menu.
- 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:
1234567891011121314151617val 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:
12val chatClient = ChatClient.instance() chatClient.devToken(userId = "ironman")
Manually Generate Tokens
To manually generate a JWT token:
- Go to your Stream dashboard.
- Copy your Stream Secret.
- 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
From the payload, you will get an instance of Result that includes your request’s status code and a request body.
Here is a basic example of connecting to the Stream server by using connectUser
:
123456789client.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:
12345678910111213141516171819202122class 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()) } }.flowOn(dispatcher) fun disconnectUser() { chatClient.disconnect() } }
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 theResult
response is to useonSuccess
andonError
extensions like the below example:
123456val 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.
12345678class 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.
First, look at the fragment_channel_list.xml layout file.
123456789101112131415161718192021222324252627282930<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 android:id="@+id/channelListView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/channelListHeaderView" app:streamUiBackgroundLayoutColor="@color/background" app:streamUiChannelListBackgroundColor="@color/background" app:streamUiChannelOptionsEnabled="true" app:streamUiChannelTitleTextSize="19sp" app:streamUiLastMessageTextSize="14sp" app:streamUiLoadingView="@layout/channels_loading_view" /> </androidx.constraintlayout.widget.ConstraintLayout>
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.
12345678910private 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:
1234567891011channelListHeaderView.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:
1private val streamChannelListUIComponent by streamChannelListComponent()
This is an advanced example to reduce initialization relevant codes by implementing a new class StreamChannelListUIComponent and a lazy extension.
1234567private 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:
- The
StreamChannelListUIComponent
initializes theChannelListViewModelFactory
andChannelListViewModel
lazily. - The
ChannelListView
will be bound to theChannelListViewModel
in theStreamChannelListUIComponent
by invoking thebindLayout
method on the Fragment’sonViewCreated
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:
12345678910111213141516171819202122232425262728293031323334353637<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" android:layout_height="0dp" android:clipToPadding="false" app:layout_constraintBottom_toTopOf="@+id/messageInputView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/messageListHeaderView" app:streamUiFlagMessageConfirmationEnabled="true" app:streamUiMessageTextColorDateSeparator="@color/white" app:streamUiMessageTextSizeUserName="15sp" app:streamUiMessageTextStyleUserName="bold" app:streamUiMuteUserEnabled="false" app:streamUiPinMessageEnabled="true" /> <io.getstream.chat.android.ui.message.input.MessageInputView android:id="@+id/messageInputView" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
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:
1234567891011121314// 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:
1234567channelListView.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:
1private val streamMessageListComponent by streamMessageListComponent()
This is an advanced example to reduce initialization relevant code by creating a new class StreamMessageListUIComponent and a lazy extension:
12345678private 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:
- The
StreamMessageListUIComponent
initializes theMessageListViewModelFactory
and relevantViewModels
lazily. - Those
ViewModels
will be bound with all UI components (likeMessageListView
) by invoking thebindView
method on the Fragment’sonViewCreated
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:
- GitHub - The AvengersChat Open Source Repository
- GitHub - Official Stream’s Android Chat Open Source Repository
- Stream’s Android In-App Messaging Tutorial
- Stream’s Android Chat Client Docs
- Stream’s Android UI Components Docs
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!