How to build a live commenting feature using Kotlin

Published Dec 27, 2017
How to build a live commenting feature using Kotlin

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.

In this post, we will build a basic commenting application. 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:

How-to-build-live-commenting-in-Kotlin-7.gif

Requirements

To follow along in this tutorial you will need the following requirements:

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).

How-to-build-live-commenting-in-Kotlin.png

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.

How-to-build-live-commenting-in-Kotlin-2.png

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.

How-to-build-live-commenting-in-Kotlin-3.png

Getting the Client Ready

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

implementation '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:

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

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

Here is our layout snippet:

<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:layout_alignParentBottom="true">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">
                <EditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1" />
                <Button
                    android:id="@+id/button_send"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                        android:text="Send" />
            </LinearLayout>
        </FrameLayout>
    </RelativeLayout>

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

How-to-build-live-commenting-in-Kotlin-4.png

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:

    class RecyclerViewAdapter (private val mContext: Context) 
      :RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder>() {        

        // The initial empty list used by the adapter
        private var arrayList: ArrayList<String> = ArrayList()

        // This updates the adapter list with list from MainActivity.kt which contains the messages.  
        fun setList(arrayList: ArrayList<String>) {
            this.arrayList = arrayList
            notifyDataSetChanged()
        }

        // The layout design used for each list item
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val view = LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent, false)
            return MyViewHolder(view)
        }

        // This displays the text for each list item
        override fun onBindViewHolder(holder: RecyclerViewAdapter.MyViewHolder, position: Int) { 
            holder.text.setText(arrayList.get(position))
        }

        // This returns the size of the list.
        override fun getItemCount(): Int {
            return arrayList.size
        }

        inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), 

        View.OnClickListener {
            var text: TextView = itemView.findViewById<View>(android.R.id.text1) as 
            TextView
            init {
                itemView.setOnClickListener(this)
            }

            override fun onClick(view: View) {

            }
        }
    }

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:

 apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-android-extensions'

    android {
        compileSdkVersion 26
        defaultConfig {
            applicationId "com.example.android.pushersample"
            minSdkVersion 15
            targetSdkVersion 26
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    }

    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
        implementation 'com.android.support:appcompat-v7:26.1.0'
        implementation 'com.android.support:design:26.1.0'

        // pusher depencency
        implementation 'com.pusher:pusher-java-client:1.5.0'

        // retrofit dependencies
        implementation 'com.squareup.retrofit2:retrofit:2.3.0'
        implementation 'com.squareup.retrofit2:converter-scalars:2.3.0'
        implementation 'com.squareup.retrofit2:converter-gson:2.3.0'

        // testing dependencies
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'com.android.support.test:runner:1.0.1'
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    }
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:

    interface ApiService {
        @GET("/{message}")
        fun sendMessage(@Path("message") title: String):Call<String>
    }
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:

    class RetrofitClient {
        fun getClient(): ApiService {
            val httpClient = OkHttpClient.Builder()

            val builder = Retrofit.Builder()
                    .baseUrl("http://10.0.2.2:5000/")
                    .addConverterFactory(ScalarsConverterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())

            val retrofit = builder
                    .client(httpClient.build())
                    .build()

            return retrofit.create(ApiService::class.java)
        }
    }

TIP: 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:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        // list to hold our messages
        var arrayList: ArrayList<String> = ArrayList()

        // Initialize our adapter
        val adapter = RecyclerViewAdapter(this)

        // assign a layout manager to the recycler view
        recycler_view.layoutManager = LinearLayoutManager(this)

        // assign adapter to the recycler view
        recycler_view.adapter = adapter

        // Initialize Pusher
        val options = PusherOptions()
        options.setCluster("PUSHER_APP_CLUSTER")
        val pusher = Pusher("PUSHER_APP_KEY", options)

        // Subscribe to a Pusher channel
        val channel = pusher.subscribe("my-channel")

        // this listener recieves any new message from the server
        channel.bind("my-event") { channelName, eventName, data ->
            val jsonObject = JSONObject(data)
            arrayList.add(jsonObject.getString("message"))
            runOnUiThread { adapter.setList(arrayList) }
        }
        pusher.connect()

        // We check for button clicks and if any text was inputed, we send the message
        button_send.setOnClickListener(View.OnClickListener {
            if (edit_text.text.length>0) {
                sendMessage(edit_text.text.toString())
            }
        })

    } // end of onCreate method

    fun sendMessage(message:String) {
        val call = RetrofitClient().getClient().sendMessage(message)

        call.enqueue(object : Callback<String> {
            override fun onResponse(call: Call<String>, response: Response<String>) {
                edit_text.setText("")
                hideKeyboard(this@MainActivity)
            }
            override fun onFailure(call: Call<String>, t: Throwable) {

            }
        })
    } // end of sendMessage method

    fun hideKeyboard(activity: Activity) {
        val imm = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager

        // Find the currently focused view, so we can grab the correct window token from it.
        var view = activity.currentFocus

        // If no view currently has focus, create a new one, just so we can grab a window token from it
        if (view == null) {
            view = View(activity)
        }

        imm.hideSoftInputFromWindow(view.windowToken, 0)
    } // end of hideKeybnoard method

NOTE: 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 handlee>

We will neessage:St of hp>We wivlin5 ment9 screenly.-4.png" />

onCreaage:place tptione>onCreal.enqu/code> keyspusholdryst_ime, vis. remembview a, we wild in yol.enqu/cfirest _ime, vicode> founapp ies,_*<.:aroject . >

When WegotOnC keys select

arh Nexwp>

We We are ue Pu

aro trloneu don arethis featur.1' wehas focus, // this the vth noWe wiater />WoInitialize o onewiatend ust berandlee>hp> le ly.-4.png" re(y weh commes, // thisn adaptcheck falsp>Here is olient”) to enable us sen. n.

e us sendtronsucsholf(viy ena wehame ls displaysa enu__ disk fun hiis featur.1' / fe Subs keys veur layohe methods below:

Woe> keent.fyDat weh rondeate">

When br-safe Hour Kotlin application.

Whe"#crens.kotlect -nab- from ient-ready">

Whe"#crens.kotlect -nab- from ient-ready" aria-hidden="true">