Updating content in realtime without confusing your users

Introduction

Introduction

In this tutorial, we will look at the possible ways of confusing your users when providing realtime content and also the proper ways you can achieve it using two use cases.

Realtime functionality involves making content available to users without them having to manually refresh or request an update. This functionality characterizes most modern apps.

However, there is a pit you can easily fall into in the process of implementing these realtime functionalities and that is confusing your users in the process.

Prerequisites

To follow along with this tutorial, you should have the following:

A common mistake

Making your application work and update content in realtime is invaluable. However, lets see a simple mistake that you need to watch out for when building realtime applications.

Below, we have a screen recording of a sample application:

realtime-content-demo-1

In the GIF above, when a new content comes in, irrespective of where the user was, they are taken back to the top of the page to view the contents right away.

This is a bad experience and it can be annoying to the user especially if there are hundreds of content and the user has gone all the way down.

Another mistake you can make, especially in a timeline based application, is not specifying where the new data added to the application starts. If you can, temporarily give the new content a slightly different background color that would hint the user of the application that the new content has not been consumed.

Users don’t like being interrupted, so silent updates to the content is welcome as long as there is a visual cue. If you have to alert users of new content, use a non-invasive pop-up without surprising the user as seen below:

realtime-content-twitter

Having noted the bad practices here, let us now build the right way.

Doing it the right way

There are many ways you can potentially introduce new content to your application without annoying your users. I will talk about two in this article.

The first one is by giving the user the option to show the new contents and not making the contents refresh without their knowledge. This model is used by Twitter to add new content to the timeline. The user gets a notification on the screen telling them that new content is available.

The second one is keeping new content at the bottom and is mostly used by chat clients like WhatsApp to display new content.

Let’s set up sample applications to demonstrate these methods.

Setting up Pusher Channels

To get started, we need a Pusher Channels app to use in our sample Android application. Go to your Pusher dashboard. Your dashboard should look like this:

realtime-content-pusher

Create a new channel app. You can easily do this by clicking the big Create new Channels app card at the bottom right. After you create a new app, you will be provided with the keys. Keep them around as you will need them later on.

Model one - the Twitter model

Open the Android studio and create a new application. Enter the name of your application, say TwitterModel and the package name. Make sure the Enable Kotlin Support check box is checked. Choose the minimum SDK, click Next, choose an Empty Activity template, stick with the MainActivity naming and click finish. After this, you wait for some seconds for Android Studio to prepare your project for you.

Next, we will add the necessary dependencies in the app-module build.gradle file. Add them like so:

1// File: ./app/build.gradle
2    dependencies {
3        // [...]
4        
5        implementation 'com.pusher:pusher-java-client:1.8.0'
6        implementation 'com.android.support:recyclerview-v7:28.0.0'
7    }

Above, we added the Pusher client dependency and the recyclerview support dependency. Sync your Gradle files so that it can be downloaded and made available for use within the application.

Next, let’s update our layout. Remember that a layout was generated for us, named activity_main.xml . This is the layout for the MainActivity class. Paste this snippet to the layout file:

1<!-- File: ./app/src/main/res/layout/activity_main.xml -->
2    <?xml version="1.0" encoding="utf-8"?>
3    <android.support.constraint.ConstraintLayout 
4        xmlns:android="http://schemas.android.com/apk/res/android"
5        xmlns:app="http://schemas.android.com/apk/res-auto"
6        xmlns:tools="http://schemas.android.com/tools"
7        android:layout_width="match_parent"
8        android:layout_height="match_parent"
9        tools:context=".MainActivity">
10    
11        <TextView
12            app:layout_constraintTop_toTopOf="parent"
13            android:layout_width="match_parent"
14            android:layout_height="wrap_content"
15            android:text="New contents available"
16            android:textSize="16sp"
17            android:visibility="gone"
18            android:padding="10dp"
19            android:background="@android:color/darker_gray"
20            android:textColor="@android:color/white"
21            android:gravity="center"
22            android:id="@+id/textViewNewContents"
23            />
24    
25        <android.support.v7.widget.RecyclerView
26            android:layout_marginTop="10dp"
27            android:id="@+id/recyclerViewContents"
28            app:layout_constraintTop_toBottomOf="@id/textViewNewContents"
29            android:layout_width="match_parent"
30            android:layout_height="wrap_content"/>
31    
32    </android.support.constraint.ConstraintLayout>

The layout above contains a recyclerview to show the contents in a list and a textview to notify the user when there is new content. By default, the textview is hidden and only pops up when there is a new content from Pusher.

Next, we will create an adapter for our recyclerview. Create a class called RecyclerListAdapter and paste this:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/RecyclerListAdapter.kt
2    import android.support.v7.widget.RecyclerView
3    import android.view.LayoutInflater
4    import android.view.View
5    import android.view.ViewGroup
6    import android.widget.TextView
7    
8    class RecyclerListAdapter: RecyclerView.Adapter<RecyclerListAdapter.ViewHolder>() {
9    
10      private val contentList: ArrayList<String> = ArrayList()
11    
12      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
13        return ViewHolder(
14          LayoutInflater.from(parent.context)
15            .inflate(android.R.layout.simple_list_item_1, parent, false)
16        )
17      }
18    
19      override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(
20        contentList.get(position)
21      )
22    
23      override fun getItemCount(): Int = contentList.size
24    
25      fun addItem(item:String){
26        contentList.add(0,item)
27        notifyDataSetChanged()
28      }
29    
30     inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
31        private val userName: TextView = itemView.findViewById(android.R.id.text1)
32        
33        fun bind(item: String) = with(itemView) {
34          userName.text = item
35        }
36      }
37    }

This class controls the recyclerview. It manages how each list item looks using the onCreateViewHolder method, it manages the size using the getItemCount method, it adds item to each list using the onBindViewHolder method and we have a custom addItem method that makes sure the new items are added to the top of the list.

Next, we will complete the rest of the logic in the MainActivity.kt file. Paste this into the file:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
2    import android.support.v7.app.AppCompatActivity
3    import android.os.Bundle
4    import android.support.v7.widget.LinearLayoutManager
5    import android.util.Log
6    import android.view.View
7    import com.pusher.client.channel.SubscriptionEventListener
8    import com.pusher.client.Pusher
9    import com.pusher.client.PusherOptions
10    import kotlinx.android.synthetic.main.activity_main.*
11    import org.json.JSONObject
12    
13    class MainActivity : AppCompatActivity() {
14      private val newList = ArrayList<String>()
15      private val recyclerListAdapter = RecyclerListAdapter()
16    
17      override fun onCreate(savedInstanceState: Bundle?) {
18        super.onCreate(savedInstanceState)
19        
20        setContentView(R.layout.activity_main)
21        setupRecyclerView()
22        setupPusher()
23        setupClickListeners()
24      }
25    }

In this class, we have a list variable and the recyclerview adapter initialized. We then call our three main methods in the onCreate method. Let us see how these methods look like.

First, we have the setupRecyclerView method where we initialize our recyclerview and assign the adapter to it. Copy and paste the following into the class:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
2    private fun setupRecyclerView() {
3    
4      with(recyclerViewContents){
5        layoutManager = LinearLayoutManager(this@MainActivity)
6        adapter = recyclerListAdapter
7      }
8      
9      recyclerListAdapter.addItem("Hello World")
10      recyclerListAdapter.addItem("New article alert!")
11    }

As seen above, we added some dummy data to the list.

Next, we have the setupPusher method. Add the method to your class:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
2    private fun setupPusher() {
3      val options = PusherOptions()
4      options.setCluster("PUSHER_CLUSTER")
5      val pusher = Pusher("PUSHER_KEY", options)
6    
7      val channel = pusher.subscribe("my-channel")
8    
9      channel.bind("my-event") { channelName, eventName, data ->
10        Log.d("TAG",data)
11        runOnUiThread {
12          textViewNewContents.visibility = View.VISIBLE
13          newList.add(JSONObject(data).getString("message"))
14        }
15      } 
16      pusher.connect()
17    }

This is the method where we connect to our Pusher Channels instance. In this method, when a new content shows up, instead of adding it to the adapter’s list, we add it to a new list and notify the user of the presence of new content.

Replace the PUSHER placeholders with actual keys from your dashboard.

The last method is the setupClickListeners method. Add the method to your class like so:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
2    private fun setupClickListeners() {
3      textViewNewContents.setOnClickListener {
4        for (item in newList){
5          recyclerListAdapter.addItem(item)
6        }
7        
8        textViewNewContents.visibility = View.GONE
9      }
10    }

This method adds a click listener to the textview. When it is clicked, we add each new item to the top of the list using the custom adapter method we created - addItem.

Since this app requires the use of internet connection, we need to request for the internet permission. Open your AndroidManifest.xml file and add this permission:

1<!-- File: ./app/src/main/AndroidManifest.xml -->
2    <uses-permission android:name="android.permission.INTERNET"/>

With this, we are done building our app. You can now run your app! To send a sample message, open the debug console of your Pusher Channels app and send an event like so:

realtime-content-pusher-event

Here is the raw data to send to the channel my-channel and the event my-event:

1{
2      "message": "Hi there"
3    }

Your app should now work like this:

realtime-content-demo-2

Model two - the WhatsApp model

In the second model, the new items will be shown at the bottom of the screen as seen on most major chat platforms. One of the popular platforms to use this model is WhatsApp.

Sometimes, you might want to read previous messages in your chat and when new messages come in, bringing you back to the new messages when there is one is just a bad way to build an application. Instead, you can simply notify the user that there are new messages and if they please, they can go to them immediately. Let’s build a sample application to show this in action.

Create a new Android project like we did earlier but this time call it WhatsAppModel. We will still use the same dependencies as we did in the first sample.

Add these to your app-module build.gradle file:

1// File: ./app/build.gradle
2    dependencies {
3        // [...]
4    
5        implementation 'com.pusher:pusher-java-client:1.8.0'
6        implementation 'com.android.support:recyclerview-v7:28.0.0'
7    }

Open the activity_main.xml generated for you replace the contents with the following:

1<!-- File: ./app/src/main/res/layout/activity_main.xml -->
2    <?xml version="1.0" encoding="utf-8"?>
3    <FrameLayout
4        xmlns:android="http://schemas.android.com/apk/res/android"
5        xmlns:app="http://schemas.android.com/apk/res-auto"
6        xmlns:tools="http://schemas.android.com/tools"
7        android:layout_width="match_parent"
8        android:layout_height="match_parent"
9        tools:context=".MainActivity">
10    
11        <android.support.v7.widget.RecyclerView
12            android:id="@+id/recyclerViewContents"
13            app:layout_constraintTop_toTopOf="parent"
14            android:layout_width="match_parent"
15            android:layout_height="wrap_content"/>
16    
17        <TextView
18            android:layout_width="wrap_content"
19            android:layout_height="wrap_content"
20            android:padding="10dp"
21            android:textSize="16sp"
22            app:layout_constraintRight_toRightOf="parent"
23            app:layout_constraintBottom_toBottomOf="parent"
24            android:layout_margin="16dp"
25            android:background="@drawable/rounded_corner"
26            android:textColor="@android:color/black"
27            android:visibility="gone"
28            android:layout_gravity="right|bottom"
29            android:id="@+id/textViewNewContents" />
30    
31    </FrameLayout>

Here, we have a recyclerview to show the contents and a textview to notify the user of the number of new messages available. Thetextview uses a special drawable as background to distinguish it easily.

Create a new drawable named rounded_corner and paste this:

1<!-- File: ./app/src/main/res/drawable/rounded_corner.xml -->
2    <?xml version="1.0" encoding="utf-8"?>
3    <shape xmlns:android="http://schemas.android.com/apk/res/android" >
4        <stroke
5            android:width="1dp" />
6    
7        <solid android:color="#ffffff" />
8    
9        <padding
10            android:left="1dp"
11            android:right="1dp"
12            android:bottom="1dp"
13            android:top="1dp" />
14    
15        <corners android:radius="5dp" />
16    </shape>

Next, let us create an adapter for the recyclerview. Create a new class called RecyclerListAdapter and paste this:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/RecyclerListAdapter.kt
2    import android.support.v7.widget.RecyclerView
3    import android.view.LayoutInflater
4    import android.view.View
5    import android.view.ViewGroup
6    import android.widget.TextView
7    
8    class RecyclerListAdapter(private val listener:OnLastPositionReached): RecyclerView.Adapter<RecyclerListAdapter.ViewHolder>() {
9    
10      private val contentList: ArrayList<String> = ArrayList()
11    
12      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
13        return ViewHolder(
14           LayoutInflater
15             .from(parent.context)
16             .inflate(android.R.layout.simple_list_item_1, parent, false)
17        )
18      }
19    
20      override fun onBindViewHolder(holder: ViewHolder, position: Int) {
21        holder.bind(contentList[position])
22        
23        if (position == contentList.size-1){
24          listener.lastPositionReached()
25        } else {
26          listener.otherPosition()
27        }
28      }
29    
30      override fun getItemCount(): Int = contentList.size
31    
32      fun addItem(item:String) {
33        contentList.add(item)
34        notifyDataSetChanged()
35      }
36    
37      inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
38        private val userName: TextView = itemView.findViewById(android.R.id.text1)
39    
40        fun bind(item: String) = with(itemView) {
41          userName.text = item
42        }
43      }
44    
45      interface OnLastPositionReached {
46        fun lastPositionReached()
47        fun otherPosition()
48      }
49    }

This is very similar to the adapter we created earlier. The difference here is that the new items are added to the bottom and we have an interface to tell us when the recyclerview is on the last item and when it is not.

Finally, open the MainActivity and set it up like so:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
2    import android.os.Bundle
3    import android.support.v7.app.AppCompatActivity
4    import android.support.v7.widget.LinearLayoutManager
5    import android.view.View
6    import com.pusher.client.Pusher
7    import com.pusher.client.PusherOptions
8    import kotlinx.android.synthetic.main.activity_main.*
9    import org.json.JSONObject
10    
11    class MainActivity : AppCompatActivity(), RecyclerListAdapter.OnLastPositionReached {
12    
13      private var count = 0
14      private val recyclerListAdapter = RecyclerListAdapter(this)
15    
16      private var lastPosition = false
17      override fun otherPosition() {
18        lastPosition = false
19      }
20    
21      override fun lastPositionReached() {
22        lastPosition = true
23        textViewNewContents.visibility = View.GONE
24        count = 0
25      }
26    
27      override fun onCreate(savedInstanceState: Bundle?) {
28        super.onCreate(savedInstanceState)
29        setContentView(R.layout.activity_main)
30        setupClickListeners()
31        setupRecyclerView()
32        setupPusher()
33      }
34    }

The MainActivity class implements the interface from the adapter to tell us when the recyclerview is at the last position or not. We use this to update the lastPosition boolean variable in our class. Let us add the other methods called into the class.

The setupClickListeners method is setup like so:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
2    private fun setupClickListeners() {
3      textViewNewContents.setOnClickListener {
4        recyclerViewContents.scrollToPosition(recyclerListAdapter.itemCount-1)
5        textViewNewContents.visibility = View.GONE
6        count = 0
7      }
8    }

When the textview that shows the count of the new messages is clicked, it scrolls down immediately to recent messages, set the count to 0 and hides the textview.

The next method is the setupRecyclerView method. Set it up like this:

1// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
2    private fun setupRecyclerView() {
3      with(recyclerViewContents){
4        layoutManager = LinearLayoutManager(this@MainActivity)
5        adapter = recyclerListAdapter
6      }
7     
8      recyclerListAdapter.addItem("Hello World")
9      recyclerListAdapter.addItem("New article alert!")
10      recyclerListAdapter.addItem("Pusher is actually awesome")
11      recyclerListAdapter.addItem("Realtime functionality freely provided ")
12      recyclerListAdapter.addItem("Checkout pusher.com/tutorials")
13      recyclerListAdapter.addItem("You can also checkout blog.pusher.com")
14      recyclerListAdapter.addItem("Learn how to update contents properly ")
15      recyclerListAdapter.addItem("Hello World")
16      recyclerListAdapter.addItem("New article alert!")
17      recyclerListAdapter.addItem("Pusher is actually awesome")
18      recyclerListAdapter.addItem("Realtime functionality freely provided ")
19      recyclerListAdapter.addItem("Checkout pusher.com/tutorials")
20      recyclerListAdapter.addItem("You can also checkout blog.pusher.com")
21      recyclerListAdapter.addItem("Learn how to update contents properly ")
22    }

This method setups the recyclerview with a layout manager and an adapter. We also added items to the list.

The next method is the setupPusher method. Set it up like so:

1private fun setupPusher() {
2      val options = PusherOptions()
3      options.setCluster("PUSHER_CLUSTER")
4    
5      val pusher = Pusher("PUSHER_KEY", options)
6      val channel = pusher.subscribe("my-channel")
7    
8      channel.bind("my-event") { channelName, eventName, data ->
9        runOnUiThread {  
10          if (!lastPosition){
11            count ++
12            textViewNewContents.text = count.toString()
13            textViewNewContents.visibility = View.VISIBLE
14            recyclerListAdapter.addItem(JSONObject(data).getString("message"))    
15          } else {
16            recyclerListAdapter.addItem(JSONObject(data).getString("message"))
17            recyclerViewContents.scrollToPosition(recyclerListAdapter.itemCount-1)
18          } 
19        }
20      }
21      
22      pusher.connect()
23    }

This is the method where we connect to our Pusher Channels instance. When a new message is gotten, we use the lastPosition variable to know if the user is already at the end of the recyclerview, then we just append the new messages and refresh the list. If the user is not at the last position, we add a textview notification to show that there is new content.

Replace the PUSHER placeholders with actual keys from your dashboard.

Add the internet permission in the AndroidManifest.xml file like so:

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

Run the application and then go ahead to your Pusher app debug console and send new messages.

Here is the raw data to send to the channel my-channel and the event my-event:

1{
2      "message": "Hi there"
3    }

Your app should now work like this:

realtime-content-demo-3

Conclusion

In this tutorial, we have learned about updating contents the right way. We looked at a possible bad UX use case where a user can get confused. We also looked at two good UX models you can easily adapt.

Feel free to play around the GitHub repo here.