How to build a live commenting feature using Kotlin

build-live-commenting-feature-kotlin-header.png

Discover how you build a realtime commenting system with a simple implementation with Kotlin and Pusher.

Introduction

In this post, we will build a basic commenting application using Kotlin. We will assume that the user is leaving a comment to a make-believe post. Here is a screen recording of what we will be building:

When building out applications, it’s not uncommon to have a commenting feature. With live commenting, comments added will update in realtime across all devices without the user refreshing the page. Applications like Facebook already have this feature.

Requirements

To follow along in this tutorial you will need the following requirements:
Knowledge of the Kotlin programming language.
– Android Studio 3.0 installed. Download here.
– A Pusher application. Create one here.
– IntelliJ IDEA installed. Download here.

When you have all the requirements let’s start.

Create New Application on Pusher

Log into your Pusher dashboard, select apps on the left navigation bar and create a new app. Input your app name (test-app in my own case), select a cluster (eu – Ireland in my case).

When you have created the Pusher app, we will move on to creating our Kotlin application.

Creating our Android Project with Kotlin Support

Open android studio, create a new project. Insert the name of your app and Company domain name then select the “include kotlin support” checkbox to enable Kotlin in the project.

For this article, we will set the minimum supported Android version at 4.03 (API 15). Next, choose an empty activity template and click on Finish.

Getting the Client Ready

Add the pusher dependency in your app build.gradle file:

1implementation 'com.pusher:pusher-java-client:1.5.0'

Our layout file will contain:

  • A recycler view (to display the comments).
  • An edit-text view (to input our message).
  • A button (to trigger an action to send a message).

A default project is created with the recycler view dependencies, however, look out for this dependency:

1implementation 'com.android.support:design:26.1.0'

and if you don’t find it, add it.

Here is our layout snippet:

1<?xml version="1.0" encoding="utf-8"?>
2    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3        xmlns:tools="http://schemas.android.com/tools"
4        android:layout_width="match_parent"
5        android:layout_height="match_parent">
6
7        <android.support.v7.widget.RecyclerView
8            android:id="@+id/recycler_view"
9            android:layout_width="match_parent"
10            android:layout_height="match_parent" />
11        <FrameLayout
12            android:layout_width="match_parent"
13            android:layout_height="?attr/actionBarSize"
14            android:layout_alignParentBottom="true">
15            <LinearLayout
16                android:layout_width="match_parent"
17                android:layout_height="wrap_content"
18                android:orientation="horizontal">
19                <EditText
20                    android:layout_width="match_parent"
21                    android:layout_height="wrap_content"
22                    android:layout_weight="1" />
23                <Button
24                    android:id="@+id/button_send"
25                    android:layout_width="wrap_content"
26                    android:layout_height="wrap_content"
27                        android:text="Send" />
28            </LinearLayout>
29        </FrameLayout>
30    </RelativeLayout>

This is what our app looks like at the moment. It is very bare with no comments yet:

We then create a recycler view adapter class named RecyclerViewAdapter.kt . This adapter is a class that handles the display of items in a list.

Paste the code below into our new class:

1class RecyclerViewAdapter (private val mContext: Context) 
2      :RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder>() {        
3
4        // The initial empty list used by the adapter
5        private var arrayList: ArrayList<String> = ArrayList()
6
7        // This updates the adapter list with list from MainActivity.kt which contains the messages.  
8        fun setList(arrayList: ArrayList<String>) {
9            this.arrayList = arrayList
10            notifyDataSetChanged()
11        }
12
13        // The layout design used for each list item
14        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
15            val view = LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent, false)
16            return MyViewHolder(view)
17        }
18
19        // This displays the text for each list item
20        override fun onBindViewHolder(holder: RecyclerViewAdapter.MyViewHolder, position: Int) { 
21            holder.text.setText(arrayList.get(position))
22        }
23
24        // This returns the size of the list.
25        override fun getItemCount(): Int {
26            return arrayList.size
27        }
28
29        inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), 
30
31        View.OnClickListener {
32            var text: TextView = itemView.findViewById<View>(android.R.id.text1) as 
33            TextView
34            init {
35                itemView.setOnClickListener(this)
36            }
37
38            override fun onClick(view: View) {
39
40            }
41        }
42    }

We will need the Retrofit library (a “type-safe HTTP client”) to enable us send messages to our remote server which we will build later on.

After adding the retrofit dependencies, your app build.gradle file should look like this:

1apply plugin: 'com.android.application'
2    apply plugin: 'kotlin-android'
3    apply plugin: 'kotlin-android-extensions'
4
5    android {
6        compileSdkVersion 26
7        defaultConfig {
8            applicationId "com.example.android.pushersample"
9            minSdkVersion 15
10            targetSdkVersion 26
11            versionCode 1
12            versionName "1.0"
13            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14        }
15        buildTypes {
16            release {
17                minifyEnabled false
18                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19            }
20        }
21    }
22
23    dependencies {
24        implementation fileTree(dir: 'libs', include: ['*.jar'])
25        implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
26        implementation 'com.android.support:appcompat-v7:26.1.0'
27        implementation 'com.android.support:design:26.1.0'
28
29        // pusher depencency
30        implementation 'com.pusher:pusher-java-client:1.5.0'
31
32        // retrofit dependencies
33        implementation 'com.squareup.retrofit2:retrofit:2.3.0'
34        implementation 'com.squareup.retrofit2:converter-scalars:2.3.0'
35        implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
36
37        // testing dependencies
38        testImplementation 'junit:junit:4.12'
39        androidTestImplementation 'com.android.support.test:runner:1.0.1'
40        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
41    }

Next, create an API Interface file in the src/main/kotlin folder called ApiService.kt. This interface is used to define endpoints to be used during network calls. For this application, we will create just one endpoint:

1interface ApiService {
2        @GET("/{message}")
3        fun sendMessage(@Path("message") title: String):Call<String>
4    }

Create a Retrofit Client class in the src/main/kotlin folder called RetrofitClient.kt. This class gives us an instance of Retrofit for our network calls:

1class RetrofitClient {
2        fun getClient(): ApiService {
3            val httpClient = OkHttpClient.Builder()
4
5            val builder = Retrofit.Builder()
6                    .baseUrl("http://10.0.2.2:5000/")
7                    .addConverterFactory(ScalarsConverterFactory.create())
8                    .addConverterFactory(GsonConverterFactory.create())
9
10            val retrofit = builder
11                    .client(httpClient.build())
12                    .build()
13
14            return retrofit.create(ApiService::class.java)
15        }
16    }

? We are using the address **10.0.2.2** because this is how the Android default emulator recognises localhost. So the IP address refers to a local server running on your machine.

We now move to our MainActivity.kt file and update it with the methods below:

1override fun onCreate(savedInstanceState: Bundle?) {
2        super.onCreate(savedInstanceState)
3
4        setContentView(R.layout.activity_main)
5
6        // list to hold our messages
7        var arrayList: ArrayList<String> = ArrayList()
8
9        // Initialize our adapter
10        val adapter = RecyclerViewAdapter(this)
11
12        // assign a layout manager to the recycler view
13        recycler_view.layoutManager = LinearLayoutManager(this)
14
15        // assign adapter to the recycler view
16        recycler_view.adapter = adapter
17
18        // Initialize Pusher
19        val options = PusherOptions()
20        options.setCluster("PUSHER_APP_CLUSTER")
21        val pusher = Pusher("PUSHER_APP_KEY", options)
22
23        // Subscribe to a Pusher channel
24        val channel = pusher.subscribe("my-channel")
25
26        // this listener recieves any new message from the server
27        channel.bind("my-event") { channelName, eventName, data ->
28            val jsonObject = JSONObject(data)
29            arrayList.add(jsonObject.getString("message"))
30            runOnUiThread { adapter.setList(arrayList) }
31        }
32        pusher.connect()
33
34        // We check for button clicks and if any text was inputed, we send the message
35        button_send.setOnClickListener(View.OnClickListener {
36            if (edit_text.text.length>0) {
37                sendMessage(edit_text.text.toString())
38            }
39        })
40
41    } // end of onCreate method
42
43    fun sendMessage(message:String) {
44        val call = RetrofitClient().getClient().sendMessage(message)
45
46        call.enqueue(object : Callback<String> {
47            override fun onResponse(call: Call<String>, response: Response<String>) {
48                edit_text.setText("")
49                hideKeyboard(this@MainActivity)
50            }
51            override fun onFailure(call: Call<String>, t: Throwable) {
52
53            }
54        })
55    } // end of sendMessage method
56
57    fun hideKeyboard(activity: Activity) {
58        val imm = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
59
60        // Find the currently focused view, so we can grab the correct window token from it.
61        var view = activity.currentFocus
62
63        // If no view currently has focus, create a new one, just so we can grab a window token from it
64        if (view == null) {
65            view = View(activity)
66        }
67
68        imm.hideSoftInputFromWindow(view.windowToken, 0)
69    } // end of hideKeybnoard method

⚠️ You will need to replace the **PUSHER_APP_*** keys with the credentials found in your Pusher application dashboard.

In the onCreate method, we initialised the list to hold the messages, the recycler view adapter to handle the display of items on the list, and assigned the recycler view accordingly.

We then initialised PusherOptions and Pusher objects with the necessary parameters. Remember to set the Pusher objects first parameter with your own app key. Your app keys can be found on the App Keys tab of the app you created. If you have forgotten the cluster you chose when creating the app, you can also find it there.

Next, we create a listener for events on that channel. When a new message is received, it will be added to our list and the updated list will be assigned to our adapter so that it can be displayed immediately.

Finally, we added a listener to the button in our layout to enable us send messages. After messages are successfully sent, we clear the text and hide the keyboard.

Next up is to add the Internet permission in your AndroidManifest.xml file. Update the file with the code snippet below:

1<uses-permission android:name="android.permission.INTERNET"/>

With this change, we are done building our client application.

Building our Kotlin Back-end Server

Our server will be built with Kotlin and hosted locally. You can follow the steps below to quickly get your server running.

Create a new Gradle based Kotlin project in IntelliJ IDEA.

Enter a “groupId” for your app. A groupId can be a package name and it’s usually something like “com.example”.

Next, enter an “artifactId”, it’s usually something like “pusher-server”

In our project build.gradle file, we will add Ktor and pusher server dependencies. Ktor is a framework for building servers and clients in connected systems using the Kotlin programming language.

Here is our complete build.gradle file which includes all the dependencies we need:

1group 'com.example'
2    version '1.0-SNAPSHOT'
3
4    buildscript {
5        // dependency version variables
6        ext.kotlin_version = '1.1.4-3'
7        ext.ktor_version = '0.2.4'
8        repositories {
9            mavenCentral()
10        }
11        dependencies {
12            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13        }
14    }
15
16    apply plugin: 'application'
17    apply plugin: 'kotlin'
18
19    sourceCompatibility = 1.8
20
21    repositories {
22        mavenCentral()
23        maven {
24            url 'http://dl.bintray.com/kotlin/kotlinx.support'
25        }
26        maven {
27            url 'http://dl.bintray.com/kotlin/ktor'
28        }
29    }
30
31    mainClassName = 'org.jetbrains.ktor.jetty.DevelopmentHost'
32
33    dependencies {
34        compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
35        // ktor dependencies
36        compile "org.jetbrains.ktor:ktor-core:$ktor_version"
37        compile "org.jetbrains.ktor:ktor-locations:$ktor_version"
38        runtime "org.jetbrains.ktor:ktor-jetty:$ktor_version"
39        // pusher server dependency
40        compile "com.pusher:pusher-http-java:1.0.0"
41        testCompile group: 'junit', name: 'junit', version: '4.12'
42    }
43
44    compileKotlin {
45        kotlinOptions.jvmTarget = "1.8"
46    }
47
48    compileTestKotlin {
49        kotlinOptions.jvmTarget = "1.8"
50    }
51
52In your `src/main/kotlin` folder, create a `Main.kt` file and insert this snippet:
53
54``` language-java
55    fun Application.main() {
56
57        val pusher = Pusher("PUSHER_APP_ID", "PUSHER_APP_KEY", "PUSHER_APP_SECRET")
58        pusher.setCluster("PUSHER_APP_CLUSTER")
59
60        install(DefaultHeaders)
61        install(CallLogging)
62        install(Routing) {
63            get("/{message}") {
64                val i = call.parameters["message"]!!
65                pusher.trigger("my-channel", "my-event", Collections.singletonMap("message", i))
66                call.respond("response sent")
67            }
68
69        }
70    }

⚠️ You will need to replace the **PUSHER_APP_*** keys with the credentials found in your Pusher application dashboard.

In the above snippet, we have defined a route that handles new messages. When a message is received, it sends the message to the Pusher channel so it can be picked up by any event listeners on the same channel.

Next, open the src/main/resources/application.conf file and set the port to 5000. If the file does not exist, create it and insert this snippet:

1ktor {
2        deployment {
3            environment = development
4            port = 5000
5        }
6
7        application {
8            modules = [com.example.MainKt.main]
9        }
10    }

This file allows you configure the server parameters.

After that, open the Terminal on the IDE, and type ./gradlew run to run the server. To test your server, open http://localhost:5000/message and you should see a display saying “response sent”.

Now we’re done with everything. We can make comments and receive updates with no stress, thanks to Pusher.

Conclusion

In this article, we have demonstrated how to work with Pusher and Kotlin while creating the commenting system. It is a very simple implementation and, of course, you can do more. I am curious to see what you will come up with.

If you have a question or feedback, leave them below in the comment section. The source code for the application is available on GitHub.