🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
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

Realtime notifications for chat messages - Building the Android app

  • Neo Ighodaro

July 24th, 2019
You will need to have Android Studio v.4+, Node.js and NPM installed on your machine.

Introduction

As seen in popular chat platforms, if a user misses a message, a notification is sent. With Chatkit, we don’t even have to spend time implementing this as it is provided out of the box.

In the tutorial, you will see how to send notifications to users when they miss messages. Here is a demo of what you will build:

Prerequisites

To follow the tutorial, you need to have the following:

  • Android Studio v3.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 Chatkit account. Create one here.

Setting up our Chatkit instance

Go to your Chatkit dashboard and click CREATE to create a new Chatkit instance. Name it chatkit-notifications.

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 private room named general and add the two users to the room.

Setting up our backend

We will build a basic server that will provide an authentication token for our users. We will build this with Node.js. Follow the following instructions to set up your server.

Create a new folder named chatkit-notifications. This will be the project root.

Next, create a package.json and paste this:

    // File: ./package.json
    {
      "name": "chatkit-notifications",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "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:

    // 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);
    });

    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 one endpoint - /token which will provide a token to a Chatkit user.

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, run your project by running this command:

    $ node index.js

Because we need to use an actual server to deliver push notifications, we will use ngrok to setup our server so that we can get a temporary live url. To get started, head to ngrok’s dashboard and create an account.

After that, follow the getting started guide found on your dashboard. You will be required to setup an auth token on your machine before you can start an HTTP tunnel. When you have completed the process, you should have something like this:

Keep this open as we will need it later on.

Creating our Android project

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 ChatkitPushNotifications, 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, you will have a default MainActivity created for you.

Setting up notifications in Chatkit

To set up notifications in a Chatkit instance, we need an FCM key and we can acquire this from a Firebase project. Go to your Firebase console and click Add project. Add the name of the project, read and accept the terms and conditions. In our own case, the name is Chatkit Notifications.

After this, you will be directed to the project overview screen. Choose the Add Firebase to your Android app option. The next screen will require the package name of your app. You can find your app’s package name is your app-module build.gradle file. Look out for the applicationId value. In our own case, the package name is com.neo.chatkitpushnotifications.

When you enter the package name and click next, you will be prompted to download a google-services.json file. Download the file and skip the rest of the process.

Add the downloaded file to the app folder of your project - ChatkitPushNotifications/app. To get the FCM key, go to your project settings on Firebase, under the Cloud Messaging tab, copy out the server key.

Now that you have your FCM key, open the Chatkit instance you created earlier, go to the Push Notifications tab. Scroll to where you have the GOOGLE FCM INTEGRATION and enter your key there. After entering your key, click the Update button to effect changes.

Building the application

As usual, the first thing we will add is our 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.google.firebase:firebase-messaging:17.1.0'
        implementation 'com.pusher:chatkit-android:1.3.3'
    }
    apply plugin: 'com.google.gms.google-services'

Here, we added a recycler view dependency since we will need to make use of lists, the firebase-messaging for push notifications and the Chatkit dependency.

Then, open your project-level build.gradle file and add the google-services classpath like so:

    // File: ./build.gradle
    dependencies {
        // [...]
        classpath 'com.google.gms:google-services:4.0.1'
    }

After this, sync the gradle files so that the new dependencies will be downloaded for you. 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">

When the project was generated, a MainActivity file was created for you. We will design the login page in this class. Open the activity_main.xml file 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"
                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>

This layout file has an EditText field which we will use to collect the user’s username and a Button which will trigger a login action. The logic for this layout will be added in the MainActivity class.

Open the class and replace it with this snippet:

    // File: app/src/main/java/com/neo/chatkitpushnotifications/MainActivity.kt
    import android.content.Intent
    import android.os.Bundle
    import android.util.Log
    import androidx.appcompat.app.AppCompatActivity
    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 = "http://10.0.2.2:3000/token",
                        userId = username.text.toString()
                    ),
                    context = this.applicationContext
                )
            )

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

                                is Error -> Log.e("Error", "Error starting Push Notifications")
                            }
                        }
                    }

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

        }


    }

Replace the CHATKIT_INSTANCE_LOCATOR holder with the instance locator from your dashboard. You can also replace the base URL with your ngrok HTTPS URL if you are testing on a real device.

In this class, we add a click listener to the loginButton. When the user clicks the button, we create a chatManager instance and connect to Chatkit on that instance. When the connection is successful, we save the result to our currentUser object in the ChatkitApp class. Next, we enable push notifications for the logged in user and finally, we open a new activity - ChatRoomActivity.

While creating a chatManager instance, we passed our token endpoint and the instance locator for our Chatkit app. Make sure you replace the instance locator holder with the instance locator on your dashboard.

Create the ChatkitApp class and add this snippet:

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

    class ChatkitApp {
        companion object {
            lateinit var currentUser: CurrentUser
        }
    }

We need this class so we can use keep a reference to our currentUser object across the app. Next, create another activity named ChatRoomActivity. Use the EmptyActivity template when creating it.

After creating the activity, a layout file - activity_chat_room.xml will be generated for you. Open the activity_chat_room.xml 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>

This layout will show a list of messages, hence the need for the RecyclerView. We also have an EditText field and a Button to enable us to send new messages to the group.

Next, we will create an adapter class for the RecyclerView to manage the messages from the room. Create a new class named ChatRoomAdapter and paste this:

    // File: app/src/main/java/com/neo/chatkitpushnotifications/ChatRoomAdapter.kt
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    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 : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {

        private var messageList = ArrayList<Message>()

        fun addMessage(model:Message){
            this.messageList.add(model)
            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)

            fun bind(item: Message) = with(itemView) {
                username.text = item.sender.name
                when (val data = item.parts[0].payload){
                    is Payload.Inline -> {
                        message.text = data.content
                    }
                }
            }
        }
    }

This adapter is in charge of displaying the messages in the activity. The adapter overrides the following methods:

  • onCreateViewHolder - To know the layout design for each row of the list. Here, we are using a custom layout - chat_list_row. Create a new layout named chat_list_row.xml and paste this in the file:
    <?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="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/editTextUsername"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                tools:text="Neo"
                android:textColor="@android:color/black"
                android:textSize="18sp"/>

        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/editTextMessage"
                app:layout_constraintTop_toBottomOf="@id/editTextUsername"
                app:layout_constraintStart_toStartOf="parent"
                tools:text="Hey there!"
                android:textSize="16sp"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
  • getItemCount - to know the size of the list. Here, we just return the size of the array list object.
  • onBindViewHolder - to bind data to each row of the list. In this app, we are only dealing with text messages and so, the bind method of the ViewHolder class only checks when it is a text message.

We have a custom method addMessage to help us add item to the adapter’s list and refresh it.

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

    import android.app.Activity
    import android.os.Bundle
    import android.view.View
    import android.view.inputmethod.InputMethodManager
    import android.widget.EditText
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.content.ContextCompat.getSystemService
    import androidx.recyclerview.widget.DividerItemDecoration
    import androidx.recyclerview.widget.LinearLayoutManager
    import com.neo.chatkitpushnotifications.ChatkitApp.Companion.currentUser
    import com.pusher.chatkit.messages.multipart.NewPart
    import com.pusher.chatkit.rooms.RoomListeners
    import kotlinx.android.synthetic.main.activity_chat_room.*

    class ChatRoomActivity : AppCompatActivity() {

        private lateinit var textViewMessage: EditText
        private val chatRoomAdapter = ChatRoomAdapter()

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

    }

This snippet contains all the imports we need and the class definition itself. Inside the 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 -> // Result<Int, Error>
                        // 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)
    }

Now, our application is complete. You can run it, and you will have something like so:

Conclusion

In this first part, we have successfully created an Android app that sends push notifications from a chat room when a user is offline. Feel free to play around with the project and add more stuff. You can extend this feature to build awesome chat apps.

In the next part we will build an iOS equivalent.

Clone the project repository
  • Android
  • Chat
  • iOS
  • Kotlin
  • Node.js
  • JavaScript
  • Swift
  • 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.