You will need the following installed: Java JDK (8 or above), Gradle (4.7 or above), the latest version of Android Studio (3.1.2 at the time of writing), and two Android emulators or devices to test. You should have at least an upper-beginner knowledge of Android development, including using Gradle and Android Studio.

Mobile messaging apps are popular, but they don’t have to be difficult to implement.

In this tutorial, you’ll learn how to build a basic but good-looking chat app for Android using Kotlin and Pusher.

In the first screen, the user enters a username. Then, when the user enters a message, the application sends it to an endpoint of server-side API, which publishes the information of the message to a Pusher channel so all the connected clients can receive it.

You’ll use Spring Boot and Kotlin to build the endpoint of the API.

For reference, here is a GitHub repository with all the code shown in this tutorial and instructions to run it.

Prerequisites

Here’s what you need to have installed/configured to follow this tutorial:

Java JDK (8 or superior)

Gradle (4.7 or superior)

The latest version of Android Studio (at the time of this writing 3.1.2)

Two Android emulators or two devices to test the app (because there’s no fun in chatting alone)

Optionally, a Java IDE with Kotlin support like IntelliJ IDEA Community Edition

I also assume that you are familiar with:

Android development (an upper-beginner level at least)

Kotlin

Android Studio

Now let’s start by creating a Pusher application.

Creating a Pusher application

If you haven’t already, create a free account at Pusher.

Then, go to your dashboard and create a Channels app, choosing a name, the cluster closest to your location, and optionally, Android as the frontend tech and Java as the backend tech:

This won’t lock you in an Android/Java stack, it will only give you some sample code to get started:

Save your app id, key, secret and cluster values, you’ll need them later. You can also find them in the App Keys tab.

Building the server-side API

Go to https://start.spring.io/ and choose to create a project with the following options:

A Gradle project

With Kotlin

Spring Boot 2.0.1 (or above version)

The project metadata of your preference

And the Web dependency

This is how the screen should look like:

Generate the project and unzip the downloaded file.

You can open the project in an IDE but it’s not really necessary. You’re only going to make three changes to the project.

Firs, add the Pusher dependency at the end of the file build.gradle :

dependencies { ... compile ( "com.pusher:pusher-http-java:1.0.0" ) }

Next, in src/main/kotling/com/example/demo , create the classes Message.kt and MessageController.kt .

Message.kt is a data class for the chat messages:

data class Message ( var user:String, var message:String, var time: Long ) `MessageController.kt` is a REST controller that defines a POST endpoint to publish the received message object to a Pusher channel (`chat`): import com.pusher.rest.Pusher import org.springframework.http.ResponseEntity import org.springframework.web.bind. annotation .* class MessageController { private val pusher = Pusher( "PUSHER_APP_ID" , "PUSHER_APP_KEY" , "PUSHER_APP_SECRET" ) init { pusher.setCluster( "PUSHER_APP_CLUSTER" ) } fun postMessage ( message: Message ) : ResponseEntity< Unit > { pusher.trigger( "chat" , "new_message" , message) return ResponseEntity.ok().build() } }

As you can see, the Pusher object is configured when the class is initialized, just replace your app information from your dashboard.

And that’s it.

Now let’s build the Android app.

Setting up the Android project

Open Android Studio and create a new project with Kotlin support:

We're not going to use anything special, so we can safely support a low API level:

Next, create an initial empty activity:

And use the default name of MainActivity with backward compatibility:

Once everything is set up, let's install the dependencies the app is going to use. In the dependencies section of the build.gradle file of your application module add:

dependencies { ... implementation 'com.pusher:pusher-java-client:1.8.0' implementation 'com.android.support:recyclerview-v7:27.1.1' implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' ... }

At the time of writing, the latest SDK version is 27, so that's my target SDK version when specifying the RecyclerView 's version.

Make sure this version number matches the version of the appcompat library:

dependencies { ... implementation 'com.android.support:appcompat-v7:27.1.1' ... implementation 'com.android.support:recyclerview-v7:27.1.1' ... }

Besides Pusher and RecyclerView to show the chat messages, the app is going to use Retrofit to make a request to the API with Moshi for serialization to and from JSON.

Sync the Gradle project so the modules can be installed and the project built.

Now let's add the INTERNET permission to the AndroidManifest.xml file. This is required so we can connect to Pusher and get the events in realtime:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.pusher.pusherchat"> <uses-permission android:name="android.permission.INTERNET" /> <application ... </application> </manifest>

And the project is all set up. Let’s start building the app.

Building the Android app

In the java directory, let’s create the data class for the messages, com.pusher.pusherchat.Messages.kt , with the same properties as the API version:

data class Message ( var user:String, var message:String, var time: Long )

If you haven’t work with Retrofit, you must know it works by turning an API into an interface.

So create the interface com.pusher.pusherchat.ChatService (your package may be different) and paste the following code:

import retrofit2.Call import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.http.POST import retrofit2.http.Body interface ChatService { fun postMessage ( body: Message ) : Call< Void > companion object { private const val BASE_URL = "http://10.0.2.2:8080/" fun create () : ChatService { val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .build() return retrofit.create(ChatService:: class . java ) } } }

The interface contains the method postMessage that mimics the endpoint of the API.

As the endpoint doesn’t return a value (only a status code that will be obtained with the Response object), the method defines Call<Void> as the method return type.

If you’re wondering why it the type isn’t Call<Unit> , Retrofit doesn’t support this type natively yet. Follow this issue for more information.

The interface also includes a companion object that creates a Retrofit instance with the Moshi converter and an implementation of the API.

Notice the use of 10.0.2.2 instead of localhost . This is how the Android emulator sees localhost. If you’re going to test the app on a device or if your API endpoint resides on another server, update the IP accordingly.

Also, by default, the API will run on port 8080 .

The first screen of the app will allow the user to enter a username. In the directory res/layout open the file activity_main and replace the content with the following:

< android.support.constraint.ConstraintLayout 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" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".MainActivity" > < TextView android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "Login" android:textSize = "25dp" android:id = "@+id/loginLabel" android:gravity = "center" app:layout_constraintBottom_toTopOf = "@id/username" app:layout_constraintLeft_toLeftOf = "parent" app:layout_constraintRight_toRightOf = "parent" app:layout_constraintTop_toTopOf = "parent" app:layout_constraintVertical_chainStyle = "packed" /> < EditText android:id = "@+id/username" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:hint = "Username" android:inputType = "text" android:maxLines = "1" app:layout_constraintTop_toBottomOf = "@id/loginLabel" app:layout_constraintBottom_toTopOf = "@id/btnLogin" /> < Button android:id = "@+id/btnLogin" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "Enter" app:layout_constraintTop_toBottomOf = "@+id/username" app:layout_constraintBottom_toBottomOf = "parent" /> </ android.support.constraint.ConstraintLayout >

Using a ConstraintLayout, it will show a label with the text Login, a text box to enter the username, and a button to log the user in.

For this app, the username will be stored in an App class ( com.pusher.pusherchat.App.kt ) that will be available for all activities:

import android.app.Application class App : Application () { companion object { lateinit var user:String } }

In a more complex application, you might want to save the username to the shared preferences or in an SQLite database.

This way, the code for the main activity ( com.pusher.pusherchat.MainActivity.kt ) will look like this:

package com.pusher.pusherchat import android.content.Intent import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.widget.Toast import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity () { override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) btnLogin.setOnClickListener { if (username.text.isNotEmpty()) { val user = username.text.toString() App.user = user startActivity(Intent( this @MainActivity , ChatActivity:: class . java )) } else { Toast.makeText(applicationContext, "Username should not be empty" , Toast.LENGTH_SHORT).show() } } } }

If the username textbox is not empty, it stores the username and starts the next activity (the chat). Otherwise, an error message is shown.

Now we need the ChatActivity class, so right-click your main package ( com.pusher.pusherchat in my case) and choose from the contextual menu the option New → Activity → Empty Activity to create the activity class:

This chat app will format in a different way the messages from the current user and the messages from the other users.

In the res/drawable directory, create a new drawable resource file, my_message_bubble.xml with the following content:

< shape xmlns:android = "http://schemas.android.com/apk/res/android" android:shape = "rectangle" > < solid android:color = "#9d48e4" > </ solid > < corners android:topRightRadius = "5dp" android:radius = "40dp" > </ corners > </ shape >

This will give you a rectangle with rounded corners, however, in the case of the top right corner, the radius is smaller to give the effect that the bubble is coming from the right:

For messages coming from other users, create another file in the res/drawable directory, other_message_bubble.xml , with the following content:

< shape xmlns:android = "http://schemas.android.com/apk/res/android" android:shape = "rectangle" > < solid android:color = "#ff00ff" > </ solid > < corners android:topLeftRadius = "5dp" android:radius = "40dp" > </ corners > </ shape >

This changes the color and the radius on the top left corner to differentiate the messages of the users.

Open the layout file that was created for the ChatActivity ( res/layout/activity_chat.xml ) and replace its contents with the following:

< android.support.constraint.ConstraintLayout 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" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".ChatActivity" > < android.support.v7.widget.RecyclerView android:id = "@+id/messageList" android:layout_width = "match_parent" android:layout_height = "match_parent" android:layout_marginBottom = "55dp" android:layout_marginLeft = "10dp" android:layout_marginStart = "10dp" android:scrollbars = "vertical" app:layout_constraintTop_toTopOf = "parent" app:layout_constraintLeft_toLeftOf = "parent" app:layout_constraintRight_toRightOf = "parent" > </ android.support.v7.widget.RecyclerView > < View android:layout_width = "0dp" android:layout_height = "2dp" android:background = "@color/colorPrimaryDark" android:layout_marginBottom = "0dp" app:layout_constraintBottom_toTopOf = "@+id/layout_messageArea" app:layout_constraintLeft_toLeftOf = "parent" app:layout_constraintRight_toRightOf = "parent" /> < LinearLayout android:id = "@+id/layout_messageArea" android:layout_width = "0dp" android:layout_height = "wrap_content" android:orientation = "horizontal" android:minHeight = "48dp" android:background = "#ffffff" app:layout_constraintBottom_toBottomOf = "parent" app:layout_constraintRight_toRightOf = "parent" app:layout_constraintLeft_toLeftOf = "parent" > < EditText android:id = "@+id/txtMessage" android:hint = "Enter message" android:background = "@android:color/transparent" android:layout_gravity = "center" android:layout_marginLeft = "16dp" android:layout_marginRight = "16dp" android:layout_width = "0dp" android:layout_weight = "1" android:layout_height = "wrap_content" android:maxLines = "6" /> < Button android:id = "@+id/btnSend" android:text = "SEND" android:textSize = "14dp" android:clickable = "true" android:layout_width = "64dp" android:layout_height = "48dp" android:gravity = "center" android:layout_gravity = "bottom" /> </ LinearLayout > </ android.support.constraint.ConstraintLayout >

This defines:

A RecyclerView to show the chat messages

to show the chat messages A View that acts as a line separator

that acts as a line separator A LinearLayout that contains: An EditText where the users enter their message A Button to send the message

that contains:

It should look like this:

Now, the app is going to present different information for the messages sent by the current user and the messages sent by the rest of the users.

So create two new layout resource files for the messages of the users:

< android.support.constraint.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" > < TextView android:id = "@+id/txtMyMessage" android:text = "Hi, my message" android:background = "@drawable/my_message_bubble" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:maxWidth = "240dp" android:padding = "15dp" android:elevation = "5dp" android:textColor = "#ffffff" android:layout_marginRight = "10dp" android:layout_marginTop = "5dp" app:layout_constraintRight_toRightOf = "parent" app:layout_constraintTop_toTopOf = "parent" /> < TextView android:id = "@+id/txtMyMessageTime" android:text = "12:00 PM" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:textSize = "10sp" android:textStyle = "bold" android:layout_marginRight = "10dp" app:layout_constraintBottom_toBottomOf = "@+id/txtMyMessage" app:layout_constraintRight_toLeftOf = "@+id/txtMyMessage" /> </ android.support.constraint.ConstraintLayout >

And:

< android.support.constraint.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" android:paddingTop = "8dp" > < TextView android:id = "@+id/txtOtherUser" android:text = "John Doe" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:textSize = "12sp" android:textStyle = "bold" app:layout_constraintTop_toTopOf = "parent" android:layout_marginTop = "5dp" /> < TextView android:id = "@+id/txtOtherMessage" android:text = "Hi, John's message" android:background = "@drawable/other_message_bubble" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:maxWidth = "240dp" android:padding = "15dp" android:elevation = "5dp" android:textColor = "#ffffff" android:layout_marginTop = "4dp" app:layout_constraintTop_toBottomOf = "@+id/txtOtherUser" /> < TextView android:id = "@+id/txtOtherMessageTime" android:text = "12:00 PM" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:textSize = "10sp" android:textStyle = "bold" app:layout_constraintLeft_toRightOf = "@+id/txtOtherMessage" android:layout_marginLeft = "10dp" app:layout_constraintBottom_toBottomOf = "@+id/txtOtherMessage" /> </ android.support.constraint.ConstraintLayout >

For the current user messages, the app shows the time the message was sent and the message itself, using the bubble defined earlier as background:

For the other user’s messages, in addition to the previous information, the app shows the username of the user:

The RecyclerView will need an Adapter to provide the views that represent the messages. So let’s create a Kotlin class, com.pusher.pusherchat.MessageAdapter.kt , and go step by step building it.

First, let’s add all the import statements we’ll need after the package declaration:

package com.pusher.pusherchat import android.content.Context import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import kotlinx.android.synthetic.main.my_message.view.* import kotlinx.android.synthetic.main.other_message.view.*

Next, outside the class, let’s define two private constants to represent the two types of chat messages:

private const val VIEW_TYPE_MY_MESSAGE = 1 private const val VIEW_TYPE_OTHER_MESSAGE = 2

Usually, constants are placed in a companion object inside the class, which makes them equivalent to public static final fields in Java. But for simple use cases, like this one, you can also define them this way.

Moving on, let’s specify that this class is a subclass of a class of type RecyclerView.Adapter<MessageViewHolder> , and define MessageViewHolder as an open class that extends RecyclerView.ViewHolder and from which the two types of messages will derive:

class MessageAdapter ( val context: Context) : RecyclerView.Adapter<MessageViewHolder>() { } open class MessageViewHolder (view: View) : RecyclerView.ViewHolder(view) { open fun bind (message: Message ) {} }

This way, inside the MessageAdapter class, we can define these two subclasses that represent the views defined in the layouts:

class MessageAdapter ( val context: Context) : RecyclerView.Adapter<MessageViewHolder>() { inner class MyMessageViewHolder (view: View) : MessageViewHolder(view) { private var messageText: TextView = view.txtMyMessage private var timeText: TextView = view.txtMyMessageTime override fun bind (message: Message ) { messageText.text = message.message timeText.text = DateUtils.fromMillisToTimeString(message.time) } } inner class OtherMessageViewHolder (view: View) : MessageViewHolder(view) { private var messageText: TextView = view.txtOtherMessage private var userText: TextView = view.txtOtherUser private var timeText: TextView = view.txtOtherMessageTime override fun bind (message: Message ) { messageText.text = message.message userText.text = message.user timeText.text = DateUtils.fromMillisToTimeString(message.time) } } }

Of course, you’ll need the class DateUtils to convert the time from milliseconds (this is how the time will be handled internally) to a readable time string. This is the definition:

import java.text.SimpleDateFormat import java.util.* object DateUtils { fun fromMillisToTimeString (millis: Long ) : String { val format = SimpleDateFormat( "hh:mm a" , Locale.getDefault()) return format.format(millis) } }

Back to the MessageAdapter class, let’s add an ArrayList to store the messages and a method to add new messages to it:

class MessageAdapter ( val context: Context) : RecyclerView.Adapter<MessageViewHolder>() { private val messages: ArrayList<Message> = ArrayList() fun addMessage (message: Message ) { messages.add(message) notifyDataSetChanged() } }

This way, you can implement the method to get the item count:

class MessageAdapter ( val context: Context) : RecyclerView.Adapter<MessageViewHolder>() { override fun getItemCount () : Int { return messages.size } }

And using the username entered in the first screen, return either the VIEW_TYPE_MY_MESSAGE constant or VIEW_TYPE_OTHER_MESSAGE in the getItemViewType method:

class MessageAdapter ( val context: Context) : RecyclerView.Adapter<MessageViewHolder>() { override fun getItemViewType (position: Int ) : Int { val message = messages. get (position) return if (App.user == message.user) { VIEW_TYPE_MY_MESSAGE } else { VIEW_TYPE_OTHER_MESSAGE } } }

So in the method onCreateViewHolder , you can inflate the view according to the type of message using the appropriate layout:

class MessageAdapter ( val context: Context) : RecyclerView.Adapter<MessageViewHolder>() { override fun onCreateViewHolder (parent: ViewGroup , viewType: Int ) : MessageViewHolder { return if (viewType == VIEW_TYPE_MY_MESSAGE) { MyMessageViewHolder( LayoutInflater.from(context).inflate(R.layout.my_message, parent, false ) ) } else { OtherMessageViewHolder( LayoutInflater.from(context).inflate(R.layout.other_message, parent, false ) ) } } }

This way, the only thing that the method onBindViewHolder has to do is to invoke the bind method of the MessageViewHolder instance it receives as an argument:

class MessageAdapter ( val context: Context) : RecyclerView.Adapter<MessageViewHolder>() { override fun onBindViewHolder (holder: MessageViewHolder , position: Int ) { val message = messages. get (position) holder?.bind(message) } }

And that’s the adapter.

Now in the class com.pusher.pusherchat.ChatActivity , after the package declaration, add the import statements the class will need and a constant (for logging):

package com.example.deborah.pusherchat import android.support.v7.app.AppCompatActivity import android.os.Bundleimport android.content.Context import android.support.v7.widget.LinearLayoutManager import android.util.Log import android.view.inputmethod.InputMethodManager import android.widget.Toast import kotlinx.android.synthetic.main.activity_chat.* import java.util.* import com.pusher.client.Pusher import com.pusher.client.PusherOptions import org.json.JSONObject import retrofit2.Call import retrofit2.Callback import retrofit2.Response private const val TAG = "ChatActivity" class ChatActivity : AppCompatActivity () { }

Also in the ChatActivity class, configure a MessageAdapter instance in the following way:

class ChatActivity : AppCompatActivity () { private lateinit var adapter: MessageAdapter override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_chat) messageList.layoutManager = LinearLayoutManager( this ) adapter = MessageAdapter( this ) messageList.adapter = adapter } }

Now, when the button to send a message is pressed, if the message box is not empty, you need to:

Build a Message object

object Call the API endpoint to publish the message to a Pusher channel

Reset the input and hide the keyboard

Otherwise, show and/or log the corresponding errors.

This is done with the following code:

class ChatActivity : AppCompatActivity () { private lateinit var adapter: MessageAdapter override fun onCreate (savedInstanceState: Bundle ?) { btnSend.setOnClickListener { if (txtMessage.text.isNotEmpty()) { val message = Message( App.user, txtMessage.text.toString(), Calendar.getInstance().timeInMillis ) val call = ChatService.create().postMessage(message) call.enqueue( object : Callback< Void > { override fun onResponse (call: Call < Void >, response: Response < Void >) { resetInput() if (!response.isSuccessful) { Log.e(TAG, response.code().toString()); Toast.makeText(applicationContext, "Response was not successful" , Toast.LENGTH_SHORT).show() } } override fun onFailure (call: Call < Void >, t: Throwable ) { resetInput() Log.e(TAG, t.toString()); Toast.makeText(applicationContext, "Error when calling the service" , Toast.LENGTH_SHORT).show() } }) } else { Toast.makeText(applicationContext, "Message should not be empty" , Toast.LENGTH_SHORT).show() } } } private fun resetInput () { txtMessage.text.clear() val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputManager.hideSoftInputFromWindow( currentFocus!!.windowToken, InputMethodManager.HIDE_NOT_ALWAYS ) } }

Finally, you need to set up a Pusher instance to listen for messages and add them to the RecyclerView when one is received:

class ChatActivity : AppCompatActivity () { private lateinit var adapter: MessageAdapter override fun onCreate (savedInstanceState: Bundle ?) { setupPusher() } private fun setupPusher () { val options = PusherOptions() options.setCluster( "PUSHER_APP_CLUSTER" ) val pusher = Pusher( "PUSHER_APP_KEY" , options) val channel = pusher.subscribe( "chat" ) channel.bind( "new_message" ) { channelName, eventName, data -> val jsonObject = JSONObject( data ) val message = Message( jsonObject[ "user" ].toString(), jsonObject[ "message" ].toString(), jsonObject[ "time" ].toString().toLong() ) runOnUiThread { adapter.addMessage(message) messageList.scrollToPosition(adapter.itemCount - 1 ); } } pusher.connect() } }

Just set your Pusher app cluster and key and you’ll be ready to test the app.

Testing the app

First of all, run the API by executing the following Gradle command in the root directory of the Spring Boot application:

gradlew bootRun

Or if you’re using an IDE, execute the main class of the application, the one annotated with @SpringBootApplication ( com.example.demo.DemoApplication in my case).

Then, in Android Studio, execute your application on two Android emulators.

This is how the first screen should look like:

And start playing with the app:

You can also monitor the messages sent to Pusher in the Debug Console of your dashboard:

Conclusion

You have learned the basics of how to create a chat app with Kotlin and Pusher for Android.

From here, you can extend it in many ways:

Change the design

Show more information

Save the messages to a database

Implement a real authentication

Use presence channels to be aware of who is subscribed to the channel

Change the implementation of the API or add more functionality

Remember that all of the source code for this application is available at Github.