Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Building an Android chat application with message retraction

  • Neo Ighodaro
July 19th, 2019
You will need to have Android Studio 3.4+, Node and npm installed on your machine.

Introduction

One feature of Chatkit is the ability to retract (withdraw) a message after it has been sent and delivered. This is useful if you made a mistake or sent a wrong message. We see this feature used in a messaging app like WhatsApp. In this article, you will learn how to implement it in your Android apps. When you finish building, your app will look like this:

Prerequisites

To follow the along in this tutorial, you need to have the following requirements:

  • Have Android Studio 3.4+ installed on your machine. You can download here.
  • Have a fair knowledge of Kotlin programming language and Android development.
  • Node.js and NPM installed on your machine. Install here.
  • A Pusher Chatkit account. Create one here.

Creating a Chatkit app

Open your Chatkit dashboard and create a new instance called chatkit-retract-message.

After creating your instance, navigate to the Console tab and create two users. In our own case, we created neo and john. We did this to reduce some complexity in our app. Still, in the Console tab, create a new room named general and add the two users to the room.

Building the application's backend

We will build a server that will provide an authentication token for our users and also help us to delete messages. We will build this with Node.js. Follow the following instructions to set up your server.

Create a new folder named chatkit-retract-message. This will be the project root. Next, create a package.json and paste this:

    // File: ./package.json
    {
      "name": "chatkit-retract-message",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {},
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "@pusher/chatkit-server": "^1.1.0",
        "body-parser": "^1.18.3",
        "express": "^4.16.4"
      }
    }

This file contains a description of our project. It also contains the dependencies we will make use of too. After that, create an index.js file and paste this snippet inside it:

This file contains a description of our project. It also contains the dependencies we will make use of too. After that, create an index.js file and paste this snippet inside it:

    // File: ./index.js
    const express = require('express');
    const bodyParser = require('body-parser');
    const Chatkit = require('@pusher/chatkit-server');
    const app = express();

    const chatkit = new Chatkit.default({
      instanceLocator: 'CHATKIT_INSTANCE_LOCATOR',
      key:'CHATKIT_KEY'
    });
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.post('/token', (req, res) => {
      const result = chatkit.authenticate({
        userId: req.query.user_id
      });
      res.status(result.status).send(result.body);
    });

    app.post('/delete-message', (req, res) => {
        const { messageId } = req.body;
          chatkit.deleteMessage({
            id: messageId
          })
            .then(() => console.log('deleted'))
            .catch(err => console.error(err))
       res.end()
    });

    const server = app.listen(3000, () => {
      console.log(`Express server running on port ${server.address().port}`);
    });

This is the main file that contains endpoints for our server. In this file, we have two endpoints - /token which will provide a token to a Chatkit user, and /delete-message to delete a message in a room.

Replace the CHATKIT_* placeholders with the real keys from your Pusher Chatkit dashboard.

Install the dependencies for your Node.js application by running this command:

    $ npm install

Finally, start the Node.js server by running this command:

    $ node index.js

Creating our Android application

Open Android Studio and create a new project. You should see a wizard like this:

Select the Empty Activity template and select Next. After that, you will be required to enter the application settings:

In our case, the project will be named ChatkitRetractMessage, also, select the Use androidx.* artifacts option as this is the new naming convention in Android. Click Finish when you are done. Now, Android Studio will take some time to prepare your project for you and you will have a default MainActivity created for you.

The next thing we will add is our projects dependencies. Open your app-module build.gradle file and add these among your dependencies:

    // File: ./app/build.gradle
    dependencies {
        // Other deps
        implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
        implementation 'com.pusher:chatkit-android:1.3.3'
    }

Next, we need to enable our app to communicate with insecure APIs since we are testing with a locally hosted API. Open the AndroidManifest.xml file and add set the android:usesCleartextTraffic property to true like so:

    <!-- File: app/src/main/AndroidManifest.xml -->
    <application
          ...
          android:usesCleartextTraffic="true">

Before we start designing our screens, let us setup some helper classes. First, create a new class called ChatkitApp and paste this:

    // File: app/src/main/java/com/neo/chatkitretractmessage/ChatkitApp.kt
    import com.pusher.chatkit.CurrentUser

    object ChatkitApp {
        lateinit var currentUser: CurrentUser
        const val BASE_URL = "http://10.0.2.2:3000"
    }

We created this object to hold some values which we will use across the app. The currentUser will be assigned much later as we develop our app. The BASE_URL is the endpoint for our server. Here, since we will test with an Android emulator, we used the 10.0.2.2 address which is what the emulator recognizes as localhost.

Next, create a new interface named ApiInterface. Paste this in the file:

    // File: app/src/main/java/com/neo/chatkitretractmessage/ApiInterface.kt

    import okhttp3.ResponseBody
    import retrofit2.Call
    import retrofit2.http.*

    interface ApiInterface {

        @POST("delete-message")
        fun retractMessage(@Body body: RetractedMessage): Call<ResponseBody>

    }

This file is an interface class to be used by Retrofit. It contains the endpoints to be accessed in this app. The request body for the retractMessage uses a custom data class. Create the class like so:

    // File: app/src/main/java/com/neo/chatkitretractmessage/RetractedMessage.kt
    import com.google.gson.annotations.SerializedName

    data class RetractedMessage(

       @field:SerializedName("messageId")
       val messageId: Int? = null
    )

After that, create a file named ApiClient and paste this:

    // File: app/src/main/java/com/neo/chatkitretractmessage/ApiClient.kt
    import com.google.gson.GsonBuilder
    import okhttp3.OkHttpClient
    import retrofit2.Retrofit
    import retrofit2.converter.gson.GsonConverterFactory

    object ApiClient {

        private var retrofit: Retrofit? = null
        val client: Retrofit
            get() {
                val gson = GsonBuilder()
                    .setLenient()
                    .create()

                if (retrofit == null) {
                    retrofit = Retrofit.Builder()
                        .baseUrl(ChatkitApp.BASE_URL)
                        .addConverterFactory(GsonConverterFactory.create(gson))
                        .client(OkHttpClient())
                        .build()
                }
                return retrofit!!
            }
    }

This object will help provide us with an instance of Retrofit which we can use across the app. Now, we can start designing and implementing our screens. We will have two screens: a login screen and a chat room screen where we will receive messages to a subscribed room.

We will start off with the login screen. We will implement this in the MainActivity. Open the activity_main.xml file created for you and paste this:

    <!-- file: app/src/main/res/layout/activity_main.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="12dp"
        tools:context=".MainActivity">

        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/username"
            android:hint="Enter username"
            android:maxLines="1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <Button android:layout_width="match_parent"
            android:text="login"
            android:id="@+id/loginButton"
            app:layout_constraintTop_toBottomOf="@+id/username"
            android:layout_height="wrap_content"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

In this snippet, we have an EditText to receive the username of the user and a Button to trigger a login action. The logic for this screen is found in the MainActivity class. Go ahead and paste this in your MainActivity file:

    // File: app/src/main/java/com/neo/chatkitretractmessage/MainActivity.kt
    import android.content.Intent
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import com.pusher.chatkit.AndroidChatkitDependencies
    import com.pusher.chatkit.ChatManager
    import com.pusher.chatkit.ChatkitTokenProvider
    import kotlinx.android.synthetic.main.activity_main.*

    class MainActivity : AppCompatActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            loginButton.setOnClickListener { setupChatManager() }
        }

        private fun setupChatManager() {
            val chatManager = ChatManager(
                    instanceLocator = "CHATKIT_INSTANCE_LOCATOR",
                    userId = username.text.toString(),
                    dependencies = AndroidChatkitDependencies(
                            tokenProvider = ChatkitTokenProvider(
                                    endpoint = "${ChatkitApp.BASE_URL}/token",
                                    userId = username.text.toString()
                            ),
                            context = this.applicationContext
                    )
            )


            chatManager.connect { result ->
                when (result) {
                    is com.pusher.util.Result.Success -> {
                        ChatkitApp.currentUser = result.value
                        startActivity(Intent(this@MainActivity,ChatRoomActivity::class.java))
                        finish()
                    }

                    is com.pusher.util.Result.Failure -> {}
                }
            }

        }
    }

Replace the CHATKIT_INSTANCE_LOCATOR holder with the instance locator from your dashboard.

In this class, we added a click listener to the button. When the button is clicked, we call the setupChatManager method. In this method, we use our token endpoint, Chatkit app instance locator and the username to set up a chatManager and connect it. When the connection is successful, we save the result and open the ChatRoomActivity.

Create a new activity called ChatRoomActivity. When creating this activity, the layout file - activity_chat_room.xml must have been generated for you. Open the file and paste this:

    <!-- File: app/src/main/res/layout/activity_chat_room.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ChatRoomActivity">

        <androidx.recyclerview.widget.RecyclerView
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/editTextMessage"
            android:id="@+id/recyclerViewMessages"
            android:layout_width="match_parent"
            android:layout_height="0dp"/>

        <EditText
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/editTextMessage"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintEnd_toStartOf="@+id/sendButton"/>

        <Button android:layout_width="wrap_content"
            android:text="send"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:id="@+id/sendButton"
            app:layout_constraintStart_toEndOf="@+id/editTextMessage"
            app:layout_constraintHorizontal_bias="0.5"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

In this layout file, we have a RecyclerView that will display messages from the chat room, an EditText field to collect a new message and a button to send a new message.

Now, we will create an adapter to manage the RecyclerView we have. Create a new class called ChatRoomAdapter and paste:

    // File: app/src/main/java/com/neo/chatkitretractmessage/ChatRoomAdapter.kt
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.ImageView
    import android.widget.TextView
    import androidx.recyclerview.widget.RecyclerView
    import com.pusher.chatkit.messages.multipart.Message
    import com.pusher.chatkit.messages.multipart.Payload

    class ChatRoomAdapter (private val chatRoomListener: ChatRoomListener) 
        : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {

        private var messageList = ArrayList<Message>()

        fun addMessage(model:Message){
            val index = messageList.indexOfFirst { it.id == model.id }
            if (index >= 0){
                messageList[index] = model
            } else {
                messageList.add(model)
            }
            notifyDataSetChanged()
        }

        fun remove(position: Int){
            val message = messageList[position]
            chatRoomListener.onDeleteMessage(message.id)
            notifyDataSetChanged()
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            return ViewHolder(
                    LayoutInflater.from(parent.context)
                            .inflate(R.layout.chat_list_row, parent, false))
        }

        override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(messageList[position])

        override fun getItemCount() = messageList.size

        inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

            private val username: TextView = itemView.findViewById(R.id.editTextUsername)
            private val message: TextView = itemView.findViewById(R.id.editTextMessage)
            private val delete : ImageView = itemView.findViewById(R.id.delete_btn)

            fun bind(item: Message) = with(itemView) {
                username.text = item.sender.name
                if (item.sender.id != ChatkitApp.currentUser.id) {
                    delete.visibility = View.GONE
                } else {
                    delete.visibility = View.VISIBLE
                }

                delete.setOnClickListener{
                     // retractMessage(item.id)
                     remove(adapterPosition)

                }
                when (val data = item.parts[0].payload){

                    is Payload.Inline -> {
                        message.text = data.content
                    }

                }

            }

        }

        interface ChatRoomListener {
            fun onDeleteMessage(messageId: Int)
        }
    }

This is a typical adapter class for a RecyclerView with the usual onBindViewHolder , onCreateViewHolder , and getItemCount methods, which manage the size of the lists and data displayed on the lists.

In the onCreateViewHolder method where we define how each row looks like, we are using a custom layout - chat_list_row. Create a new layout named chat_list_row.xml and paste this in the file:

    <!-- File: app/src/main/res/layout/chat_list_row.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_margin="20dp"
        xmlns:tools="http://schemas.android.com/tools">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/editTextUsername"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/delete_btn"
            android:textColor="@android:color/black"
            android:textSize="18sp"/>

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/editTextMessage"
            app:layout_constraintTop_toBottomOf="@id/editTextUsername"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/delete_btn"
            android:textSize="16sp"/>

        <ImageView
            android:id="@+id/delete_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@android:drawable/ic_menu_delete"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

This layout contains two TextViews to display the name of the sender and the message itself. In addition to that, we have other custom methods in the adapter:

  • addMessage - This method adds a new message or updates a retracted message to the RecyclerView .
  • remove - This method triggers our ChatRoomListener's onDeleteMessage with the message ID to be deleted. This interface will be implemented by the activity.

In the bind method of our ViewHolder class, we make sure we show the delete icon to only the owner of the message and we add a click listener to trigger the remove method.

Now, let us finish things up in the ChatRoomActivity. Open the class and replace it with this:

    // File: app/src/main/java/com/neo/chatkitretractmessage/ChatRoomActivity.kt
    import android.app.Activity
    import android.os.Bundle
    import android.util.Log
    import android.view.View
    import android.view.inputmethod.InputMethodManager
    import android.widget.EditText
    import androidx.appcompat.app.AppCompatActivity
    import androidx.recyclerview.widget.DividerItemDecoration
    import androidx.recyclerview.widget.LinearLayoutManager
    import com.pusher.chatkit.messages.multipart.NewPart
    import com.pusher.chatkit.rooms.RoomListeners
    import kotlinx.android.synthetic.main.activity_chat_room.*
    import okhttp3.ResponseBody
    import retrofit2.Call
    import retrofit2.Callback
    import retrofit2.Response

    // You need to replace the "com.neo" part with your app's bundle identifier
    import com.neo.chatkitretractmessage.ChatkitApp.currentUser

    class ChatRoomActivity : AppCompatActivity(), ChatRoomAdapter.ChatRoomListener  {

        private lateinit var textViewMessage: EditText
        private val chatRoomAdapter = ChatRoomAdapter(this)
        private val api = ApiClient.client.create(ApiInterface::class.java)

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_chat_room)
            textViewMessage = findViewById(R.id.editTextMessage)
            setupRecyclerView()
            subscribeToRoom()
            setupButtonListener()
        }
    }

You need to replace the "com.neo" part with your app's bundle identifier

This snippet contains all the imports and variables we need. It also contains the class definition itself. While initializing the chatRoomAdapter, we passed the this meaning that the class will implement the interface method. Paste the method in your class like so:

    override fun onDeleteMessage(messageId: Int) {
        api!!.retractMessage(RetractedMessage(messageId)).enqueue(object : Callback<ResponseBody> {
            override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
                Log.e("TAG", t.message)
            }

            override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
                if (response.isSuccessful){
                    Log.e("TAG", response.message())
                }
            }

        })
    }

When this method is called, we make an API call to delete that message. When the message is deleted, Chatkit updates that message and sends it to the onMultipartMessage callback provided for us when we subscribe to a room.

Inside the ChatRoomActivity class, we override the onCreate method provided by the Android SDK. Inside this method, we call other methods that you will now create:

  • setupRecyclerView() - This method will set up our RecyclerView as the name implies. Create the method inside the class like so:
    private fun setupRecyclerView() {
      with(recyclerViewMessages){
        layoutManager = LinearLayoutManager(this@ChatRoomActivity)
        adapter = chatRoomAdapter
        addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
      }
    }

In this method, we assign a layout manager and adapter to the recyclerViewMessages, and we also decorate it to show a divider after every item.

  • subscribeToRoom() - Create the method like so:
    private fun subscribeToRoom() {
      currentUser.subscribeToRoomMultipart(
        roomId = currentUser.rooms[0].id,
        listeners = RoomListeners(
          onMultipartMessage = {
            runOnUiThread {
              chatRoomAdapter.addMessage(it)
            }
          }
        ),
        messageLimit = 20, // Optional
        callback = { subscription ->
          // Called when the subscription has started.
          // You should terminate the subscription with subscription.unsubscribe()
          // when it is no longer needed
        }
      )
    }

This method will subscribe us to the general room which is the user’s first room and when a new message comes in, we tell the adapter about it.

  • setupButtonListener() - This method will add a listener to the button so that when it is clicked, we can send a new message. Create the method like so:
    private fun setupButtonListener() {
      sendButton.setOnClickListener {
        if (textViewMessage.text.isNotEmpty()) {
          currentUser.sendMultipartMessage(
            roomId = currentUser.rooms[0].id,
            parts = listOf(NewPart.Inline(textViewMessage.text.toString(), "text/plain")),
            callback = { result -> 
              // The Int is the new message ID
            }
          )

          textViewMessage.setText("")
          hideKeyboard()
        }
      }
    }

In the snippet above, we listen for when the user clicks the send button, when this happens, we check to be sure that they typed something in the EditText field. Then we go ahead to use the currentUser object and send a new message. When a message is sent, we clear the text and hide the keyboard using the hideKeyboard() method.

Create the method inside your class like so:

    private fun hideKeyboard() {
        val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
        var view = currentFocus

        if (view == null) {
            view = View(this)
        }

        imm.hideSoftInputFromWindow(view.windowToken, 0)
    }

When you run the app, you should have something like this:

Conclusion

In this article, we have learned how to implement the retracting message feature in our Android chat apps thanks to Chatkit. Feel free to dive into the GitHub repo and build on top of it.

Clone the project repository
  • Android
  • Chat
  • JavaScript
  • Kotlin
  • Node.js
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.