Note: This blog has been updated since it was originally published in May 2020
Kotlin is a relatively young language, but it is quickly gaining popularity among developers due to its convenient and modern approach to programming Android apps. In fact, in May 2019, Google announced that Kotlin was now its preferred programming language for Android app development.
Keeping pace with current trends, the team at QuickBlox have developed a feature-rich chat SDK for Android suitable for applications written in Kotlin (or Java). Our step-by-step chat SDK Android tutorial below will show you how to add stunning real-time communication features to your modern Android messaging app powered by QuickBlox.
Let’s go!
To start off with the QuickBlox SDK, you need to go through a simple registration following this link.
Now that you have an account with QuickBlox, you can proceed to the next preliminary step.
Create an application in the QuickBlox admin panel. QuickBlox application is a separate space to store your data, users, dialogs, future chat histories, custom objects, and more. To do this, simply click the “+” button.
The next step is to enter a brief description of the chat app:
As you can see on this screen, Title and Type are two required fields.
Title is the name of the application that will be displayed in the QuickBlox admin panel.
Type is the type of your application.
Please note that there can be more than one application inside your admin panel. It is recommended that you create a new QuickBlox application for every new project.
Click the “Add” button at the bottom of the page and the browser will redirect you to the screen with the application already created.
At this point, we should see the following:
Next we will need the Application ID, Authorization key, Authorization secret, and below – the Account key. All these are required for the following authentication of the client-side application using QuickBlox API.
To connect the QuickBlox chat messaging SDK for Android to your app, import QuickBlox SDK dependencies via “build.gradle” file (Module: app):
dependencies { implementation "com.quickblox:quickblox-android-sdk-messages:3.9.2" implementation "com.quickblox:quickblox-android-sdk-chat:3.9.2" implementation "com.quickblox:quickblox-android-sdk-content:3.9.2" }
The next step is to synchronize the project with Gradle files. To do this, click the appropriate button inside Android Studio – “Sync Project with Gradle Files”.
After successful synchronization of the project, the previously added dependencies will allow you to interact with the Quickblox Android SDK.
First, we start by creating a new class which extends android.app.Application as follows:
import android.app.Application import com.quickblox.auth.session.QBSettings //App credentials private const val APPLICATION_ID = "" private const val AUTH_KEY = "" private const val AUTH_SECRET = "" private const val ACCOUNT_KEY = "" private const val API_ENDPOINT = "https://your.api.endpoint.com" private const val CHAT_ENDPOINT = "your.chat.endpoint.com" //Attention! Without https:// class AppTest : Application() { override fun onCreate() { super.onCreate() checkAppCredentials() initQuickBloxSDK() } private fun checkAppCredentials() { if (APPLICATION_ID.isEmpty() || AUTH_KEY.isEmpty() || AUTH_SECRET.isEmpty() || ACCOUNT_KEY.isEmpty()) { throw AssertionError("Something wrong with credentials") } } private fun initQuickBloxSDK() { QBSettings.getInstance().init(applicationContext, APPLICATION_ID, AUTH_KEY, AUTH_SECRET) QBSettings.getInstance().accountKey = ACCOUNT_KEY // QBSettings.getInstance().setEndpoints(API_ENDPOINT, CHAT_ENDPOINT, ServiceZone.PRODUCTION) // QBSettings.getInstance().zone = ServiceZone.PRODUCTION } }
Note that this class must be registered in AndroidManifest.xml. Specify the android:name property in the the node in AndroidManifest.xml:
<application android:name=".AppTest" ... </application>
Also, since we have just signed up, we do not have our own Chat and API servers (these are only available on the Enterprise plan), so there is no need to call the method QBSettings.getInstance().setEndpoints()
and QBSettings.getInstance().zone =
The Chat and API Domain values obtained will automatically be set.
We have created a library for this purpose, which contains claim type constants (APPLICATION_ID, AUTH_KEY, ...
) to use them both in the server and the client projects. Please note that these are your Application ID, Authorization key, Authorization secret and Account key that you can find in the newly created application.
Please be careful, while copying and pasting them into the value of the corresponding constants.
Note! The QBSettings.getInstance().init
method must be called before we initialize Activities or Fragments. Which is why we had to create a class file that extends android.app.Application
.
After the initialization, a user authorization procedure can be performed.
Let’s say we have created a screen, which we correspondingly called LoginActivity
, where the application user can input, for example, username, password, and then click the “Login” button to enter the application:
<EditText android:id="@+id/et_login" android:layout_width="match_parent" android:layout_height="44dp" android:layout_marginLeft="16dp" android:layout_marginTop="11dp" android:layout_marginRight="16dp" android:paddingLeft="12dp" android:paddingRight="12dp" android:singleLine="true" android:hint="Enter Your Login" /> <EditText android:id="@+id/et_password" android:layout_width="match_parent" android:layout_height="44dp" android:layout_marginLeft="16dp" android:layout_marginTop="11dp" android:layout_marginRight="16dp" android:paddingLeft="12dp" android:paddingRight="12dp" android:singleLine="true" android:hint="Enter Your Password" /> <Button android:id="@+id/btn_login" android:layout_width="match_parent" android:layout_height="44dp" android:layout_marginLeft="16dp" android:layout_marginTop="42dp" android:layout_marginRight="16dp" android:text="Login"/>
To simplify* we apply user’s authorization with a username and password, but we can just enter an email + password.
*(Or even use the Firebase service for authorization and submit it to the server using the Firebase Project ID and Access Token.)
After the user inputs the username and password, we will need to build the QBUser
model for the following authorization and call the method of the QuickBlox Android SDK: QBUsers.signIn(...)
:
private fun prepareUser() { val qbUser = QBUser() qbUser.login = loginEt.text.toString().trim { it <= ' ' } qbUser.password = passwordEt.text.toString().trim { it <= ' ' } signIn(qbUser) } private fun signIn(user: QBUser) { QBUsers.signIn(user).performAsync(object : QBEntityCallback{ override fun onSuccess(qbUser: QBUser?, bundle: Bundle?) { // Successfully Signed In loginToChat(user) } override fun onError(e: QBResponseException?) { // Sign In Error if (e?.httpStatusCode == 401) { signUp(user) } else { Toast.makeText(this@LoginActivity, "Sign In Error: " + e?.message, Toast.LENGTH_LONG).show() } } }) }
If you correctly added the dependencies to the build.gradle file (application level), all the resources from the QuickBlox Android SDK will be available, as well as the QBUser
model and the QBUsers.signIn(...)
method that we just used. If the model is not available, it means the dependencies are not connected correctly, or the project is not synchronized.
The code section above shows that when, as a result of the QBUsers.signIn(...)
method, the onSuccess
callback is invoked, this means that we are successfully logged in the API (as such“user existed”) and now we can log in to the Chat server.
In case something went wrong, the onError
callback will be invoked. Also, as it can be seen from the code section above – one of the reasons for this may be that this user has not yet been created and the API returns error 401.
Considering this we will create a new user using QBUsers.signUp(...)
:
private fun signUp(user: QBUser) { QBUsers.signUp(user).performAsync(object : QBEntityCallback{ override fun onSuccess(qbUser: QBUser?, b: Bundle?) { // Successful Sign Up signIn(user) } override fun onError(e: QBResponseException?) { // Sign Up Error Toast.makeText(this@LoginActivity, "Sign Up Error: " + e?.message, Toast.LENGTH_LONG).show() } }) }
After creating the user (Sign Up), we must log in with the newly created user by calling signIn(...)
method again.
To configure a connection to the Chat server is essential before connecting to it:
private fun setupChatConnection() { val configurationBuilder = QBChatService.ConfigurationBuilder() configurationBuilder.socketTimeout = 300 configurationBuilder.isUseTls = true // TLS is disabled by default configurationBuilder.isKeepAlive = true configurationBuilder.isAutojoinEnabled = false configurationBuilder.setAutoMarkDelivered(true) configurationBuilder.isReconnectionAllowed = true configurationBuilder.setAllowListenNetwork(true) configurationBuilder.port = 5223 QBChatService.setConfigurationBuilder(configurationBuilder) }
Now that we are successfully logged in, let’s proceed to authorization to the Chat server:
private fun loginToChat(user: QBUser) { QBChatService.getInstance().login(user, object : QBEntityCallback{ override fun onSuccess(v: Void?, b: Bundle?) { // Chat Login Successful } override fun onError(e: QBResponseException?) { // Chat Login Error } }) }
After successful authorization to the Chat server, we can interact with the API and Chat server.
Before sending messages, you need to download the already existing dialogs: in which our user is listed among occupants:
private fun loadDialogsFromServer() { val requestBuilder = QBRequestGetBuilder() requestBuilder.limit = 100 QBRestChatService.getChatDialogs(null, requestBuilder).performAsync(object : QBEntityCallback> { override fun onSuccess(dialogs: ArrayList ?, b: Bundle?) { // Dialogs Loaded Successfully } override fun onError(e: QBResponseException?) { // Loading Dialogs Error } }) }
When dialogs are downloaded successfully, we have got the ArrayList
collection, which may be empty if the user doesn’t participate in at least one dialog.
You can save the downloaded dialog collection in any convenient and familiar way, and update as necessary. You will need it when a new dialog is created with the user, or the user was invited to an existing dialog.
It can be saved in an internal database, or in a cache, or otherwise.
We can now create a dialog with any user that exists in the current QuickBlox application, but to do this you need to download at least several users first.
The next thing you can do is download and save users who already have dialogs. This must be done in order to get user models from the server and update information about them in our internal storage– because someone could have changed their name, phone number, avatar, or other fields since the last update.
To download the models of specific users from the server, we can take the dialogs list, get participants from each dialog, create a list from the IDs of these users, and download them from the server. This can be done in the following way:
private fun prepareUsersIDsToLoad(dialogs: ArrayList) { val userIDs = HashSet () for (dialog in dialogs) { userIDs.addAll(dialog.occupants) } val requestBuilder = QBPagedRequestBuilder(100, 1) loadUsersFromDialogs(userIDs, requestBuilder) } private fun loadUsersFromDialogs(userIDs: HashSet , requestBuilder: QBPagedRequestBuilder) { QBUsers.getUsersByIDs(userIDs, requestBuilder).performAsync(object : QBEntityCallback > { override fun onSuccess(qbUsers: ArrayList ?, bundle: Bundle?) { if (qbUsers != null) { bundle?.let { val totalPages = it.get("total_pages") as Int val currentPage = it.get("current_page") as Int if (totalPages > currentPage) { requestBuilder.page = currentPage + 1 loadUsersFromDialogs(userIDs, requestBuilder) } else { // All Users From Dialogs Loaded } } } } override fun onError(e: QBResponseException?) { // Loading Users Error } }) }
Here we have initiated a list of unique values from the user IDs that are occupants in each dialog available to us. Then we have created a QBPagedRequestBuilder
with the parameters Per page = 100
and page = 1
.
Next, when calling the function onSuccess callback
(successful download), we check whether we have downloaded all the users, or if we repeat the request by increasing the “page number” by 1.
For example, if we have 146 unique users from the dialogs, our method will work twice. The first time it will load 100 users, then it will call itself again and load the remaining 46.
Now let’s download more users from the server, for example, it will be the 100 recently created users from our QuickBlox application:
private fun loadUsersFromQuickBlox() { val rules = ArrayList() rules.add(GenericQueryRule("order", "desc string created_at")) val requestBuilder = QBPagedRequestBuilder() requestBuilder.rules = rules requestBuilder.perPage = 100 requestBuilder.page = 1 QBUsers.getUsers(requestBuilder).performAsync(object : QBEntityCallback > { override fun onSuccess(usersFromServer: ArrayList ?, b: Bundle?) { } override fun onError(e: QBResponseException?) { } }) }
Please note that in order to create a dialog, you must have at least two users in your QuickBlox application. Users can be created in the admin panel, or by installing the application on several devices, after the registration of a new user (Sign Up).
If QBUsers.getUsers(...)
method is performed correctly, the onSuccess()
callback will be invoked, which indicates the successful loading of the collection ArrayList
. Along with the Dialogs list, you can store it in any convenient and familiar way, and update as necessary.
Now that we have some users, we can create a dialog with them.
Before we do this, let’s remember that there are three types of dialogs:
QBDialogType.PRIVATE
) (contains only two users, communicating one-on-one and there is no way to add anyone else to this dialog).QBDialogType.GROUP
) (any number of users can participate in the dialog, usually 3 or more. We can also create a group dialog for two users, assuming that we want to add more participants to it).QBDialogType.PUBLIC_GROUP
) (All users from your application will be able to join it. The server will create a public chat and return detailed information about the newly created dialog).Important! The public dialog model does not contain a list of user IDs. Therefore, publicDialog.occupants
doesn’t have a value.
Note. You can also create dialogs in the QuickBlox admin panel.
Let’s create a group dialog with any users from the list we have downloaded earlier:
private fun createDialog(usersIdsList: ArrayList) { val newDialog = QBChatDialog() newDialog.name = "My awesome chat" newDialog.type = QBDialogType.GROUP newDialog.setOccupantsIds(usersIdsList) QBRestChatService.createChatDialog(newDialog).performAsync(object : QBEntityCallback { override fun onSuccess(qbChatDialog: QBChatDialog?, b: Bundle?) { qbChatDialog.join(DiscussionHistory()) } override fun onError(e: QBResponseException?) { } }) }
At the beginning we created usersIdsList: ArrayList
. It’s a collection of user IDs with whom we want to create a new dialog. We can create this list, for example, in the following way:
private fun makeUsersIDsCollection(usersToCreateDialog: ArrayList): ArrayList { val usersIdsList = ArrayList () for (qbUser : QBUser in usersToCreateDialog) { usersIdsList.add(qbUser.id) } return usersIdsList }
Here usersToCreateDialog
is already the list of users with whom we want to create a dialog.
As a result, when onSuccess()
callback is triggered, we get the QBChatDialog
model of the dialog we just created. It can be added to those already stored, or again request the entire list of dialogs from the server, as we did earlier (QBRestChatService.getChatDialogs(...)
).
Note! After receiving a callback about the successful creation of the dialog (onSuccess
), we join the dialog for sending and receiving messages using: qbChatDialog.join(DiscussionHistory()
). We can join only to a Group dialog and not a Private dialog.
Now we are ready to send the first message to the new dialog.
Let’s compose a message and send it to the dialog:
private fun sendMessage(qbChatDialog: QBChatDialog, message: String) { val qbChatMessage = QBChatMessage() qbChatMessage.body = message qbChatMessage.setSaveToHistory(true) // To be able to retrieve this message in Dialog’s chat history qbChatMessage.dateSent = System.currentTimeMillis() / 1000 qbChatMessage.isMarkable = true // To be able to mark message as delivered or as read further qbChatDialog.sendMessage(qbChatMessage, object : QBEntityCallback{ override fun onSuccess(v: Void?, b: Bundle?) { // Message sent } override fun onError(e: QBResponseException?) { // Sending Error } }) }
Now, if we download the message history of this dialog, our message will appear in it.
To download the chat history, you should specify the section of messages downloaded from the server (limit
), and the number of messages that should be skipped (skip
), in case we want the next section of messages.
private fun loadChatHistory(qbChatDialog: QBChatDialog) { val messageGetBuilder = QBMessageGetBuilder() messageGetBuilder.limit = 50 messageGetBuilder.skip = 0 // To load first 50 messages messageGetBuilder.sortDesc("date_sent") // To retrieve date-send-sorted Collection messageGetBuilder.markAsRead(false) // To mark each message as Read QBRestChatService.getDialogMessages(qbChatDialog, messageGetBuilder).performAsync(object : QBEntityCallback> { override fun onSuccess(messages: ArrayList ?, b: Bundle?) { } override fun onError(e: QBResponseException?) { } }) }
As a result of a successful request to the server, onSuccess()
callback works and we get a messages collection: messages: ArrayList
.
Just as in the case of downloading Dialogs and Users (QBChatDialog, QBUser
), messages (QBChatMessage
) can be stored and updated in any way convenient for you.
Remember when we created the message, we set a parameter “qbChatMessage.setSaveToHistory(true)
”?
If the value of this parameter is false, then the message will not be saved on the server, and, accordingly, will not appear in the chat history downloaded from the server.
In the same way, we can load chat history from any dialog available to us.
Almost all the main functionality of our application is now described. Let’s see how to listen to the incoming messages.
All we need is to add a listener (QBChatDialogMessageListener
) to the Dialog or listen to all messages, not just from a specific one.
Let’s listen to only one dialog:
qbChatDialog.addMessageListener(object : QBChatDialogMessageListener { override fun processMessage(dialogId: String?, qbChatMessage: QBChatMessage?, senderId: Int?) { // Message (qbChatMessage) received from Dialog with ID = dialogId ; From user with ID = senderID } override fun processError(dialogId: String?, e: QBChatException?, qbChatMessage: QBChatMessage?, senderId: Int?) { } })
We can see this listener has two callbacks: processMessage
and processError
. In this case, we need to process events from processMessage
. These events will be incoming messages, we will get a QBChatMessage
model including all the values of the model fields set by the sender.
– Let’s listen to all the dialogs:
We can use the same listener without a reference to a particular dialog. This is required when we need to receive incoming messages from all dialogs of the user.
private fun registerAllDialogsMessageListener() { val incomingMessagesManager = QBChatService.getInstance().incomingMessagesManager incomingMessagesManager.addDialogMessageListener(object : QBChatDialogMessageListener { override fun processMessage(dialogId: String?, qbChatMessage: QBChatMessage?, senderId: Int?) { // New Message Received } override fun processError(dialogId: String?, e: QBChatException?, qbChatMessage: QBChatMessage?, senderId: Int?) { // Error Message Received } }) }
Along with all the other functionality, we need to disconnect from the Chat server when the user sends the application to the background (by minimizing it or by switching to another application), and reconnect again when necessary. For example, the user might want to add an attachment to a chat message, but when they open file explorer to pick a file, the chat application goes into the background and disconnects from the chat server. And when they return again to the application with the picked file, the app needs to be reconnected to the chat.
As soon as we disconnect from the Chat server, the server understands that we have gone “offline”, and it is necessary to interact with the user in a different way.
Push notifications notify the user about any event, whether it’s an incoming voice or video call, new message or alert, advertising promotion, a new chat dialog, a friend request for a social network, and so on. Notifications can navigate users to a specific chat or destination. The main advantage of Push notifications is that they will be received by the device even if our application is not currently running.
Push notifications are worth a separate article. See our previous blogs on this topic: What are push notifications? and How push-notifications can improve your business. For the scope of this tutorial the most important thing to know is that the Chat server, realizing that we are “offline,” for example, will monitor all messages in Private dialogs and as soon as we appear online (we launch the application, connect to the chat server and become “online” for it), the Chat server will consistently deliver to us messages that were addressed to us during our absence.
To disconnect from the chat server:
QBChatService.getInstance().logout(object : QBEntityCallback{ override fun onSuccess(aVoid: Void?, bundle: Bundle?) { QBChatService.getInstance().destroy() } override fun onError(e: QBResponseException?) { } })
And to connect to the Chat server again, as I mentioned above, you need to call (QBChatService.getInstance().login(...)
).
Now let’s say we need to sign in as another user, or just log out of the current user’s account in the application.
private fun logout() { QBUsers.signOut().performAsync(object : QBEntityCallback{ override fun onSuccess(v: Void?, b: Bundle?) { QBChatService.getInstance().destroy() // Start Login Screen finish() } override fun onError(e: QBResponseException?) { // Logout Error } }) }
As you can see, after a successful Logout from the API (QBUsers.signOut()
), we need to log out from the Chat server, and at the same time clear everything that was connected with the current user session on the Chat server (QBChatService.getInstance().destroy()
).
By following the steps in this tutorial you will be able to build an Android chat application written in Kotlin. Let’s provide a summary of these key steps so that you are clear about the logic sequence of the completed chat app:
The QuickBlox Android SDK has additional features available to support more advanced use-cases such as audio & video calling, video conferencing, push notifications, content moderation, rich messages, and more.
Check out our Kotlin chat sample and detailed Android documentation so that you can start building your Android chat app today.
For technical queries about using our SDK or recommendations for our product or documentation please submit a ticket to our support team.
Contact us if you want to learn more about how QuickBlox can support your communication needs.