Updating content in realtime without confusing your users
You will need Android Studio installed on your machine, and basic knowledge of Android development.
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:
- Android Studio installed on your machine. Download the latest stable version here.
- A Pusher application. Create one here.
- Basic knowledge of Android development and an ability to navigate through the Android Studio IDE.
- Basic knowledge of the Kotlin programming language. Visit the official docs.
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:
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:
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. Log in to your Pusher dashboard. If you don’t have an account, you can easily create one. Your dashboard should look like this:
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:
// File: ./app/build.gradle
dependencies {
// [...]
implementation 'com.pusher:pusher-java-client:1.8.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
}
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:
<!-- File: ./app/src/main/res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="New contents available"
android:textSize="16sp"
android:visibility="gone"
android:padding="10dp"
android:background="@android:color/darker_gray"
android:textColor="@android:color/white"
android:gravity="center"
android:id="@+id/textViewNewContents"
/>
<android.support.v7.widget.RecyclerView
android:layout_marginTop="10dp"
android:id="@+id/recyclerViewContents"
app:layout_constraintTop_toBottomOf="@id/textViewNewContents"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</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:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/RecyclerListAdapter.kt
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
class RecyclerListAdapter: RecyclerView.Adapter<RecyclerListAdapter.ViewHolder>() {
private val contentList: ArrayList<String> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(
contentList.get(position)
)
override fun getItemCount(): Int = contentList.size
fun addItem(item:String){
contentList.add(0,item)
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val userName: TextView = itemView.findViewById(android.R.id.text1)
fun bind(item: String) = with(itemView) {
userName.text = item
}
}
}
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:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.util.Log
import android.view.View
import com.pusher.client.channel.SubscriptionEventListener
import com.pusher.client.Pusher
import com.pusher.client.PusherOptions
import kotlinx.android.synthetic.main.activity_main.*
import org.json.JSONObject
class MainActivity : AppCompatActivity() {
private val newList = ArrayList<String>()
private val recyclerListAdapter = RecyclerListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupRecyclerView()
setupPusher()
setupClickListeners()
}
}
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:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
private fun setupRecyclerView() {
with(recyclerViewContents){
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = recyclerListAdapter
}
recyclerListAdapter.addItem("Hello World")
recyclerListAdapter.addItem("New article alert!")
}
As seen above, we added some dummy data to the list.
Next, we have the setupPusher
method. Add the method to your class:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
private fun setupPusher() {
val options = PusherOptions()
options.setCluster("PUSHER_CLUSTER")
val pusher = Pusher("PUSHER_KEY", options)
val channel = pusher.subscribe("my-channel")
channel.bind("my-event") { channelName, eventName, data ->
Log.d("TAG",data)
runOnUiThread {
textViewNewContents.visibility = View.VISIBLE
newList.add(JSONObject(data).getString("message"))
}
}
pusher.connect()
}
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:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
private fun setupClickListeners() {
textViewNewContents.setOnClickListener {
for (item in newList){
recyclerListAdapter.addItem(item)
}
textViewNewContents.visibility = View.GONE
}
}
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:
<!-- File: ./app/src/main/AndroidManifest.xml -->
<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:
Here is the raw data to send to the channel my-channel
and the event my-event
:
{
"message": "Hi there"
}
Your app should now work like this:
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:
// File: ./app/build.gradle
dependencies {
// [...]
implementation 'com.pusher:pusher-java-client:1.8.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
}
Open the activity_main.xml
generated for you replace the contents with the following:
<!-- File: ./app/src/main/res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerViewContents"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="16sp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="16dp"
android:background="@drawable/rounded_corner"
android:textColor="@android:color/black"
android:visibility="gone"
android:layout_gravity="right|bottom"
android:id="@+id/textViewNewContents" />
</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:
<!-- File: ./app/src/main/res/drawable/rounded_corner.xml -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke
android:width="1dp" />
<solid android:color="#ffffff" />
<padding
android:left="1dp"
android:right="1dp"
android:bottom="1dp"
android:top="1dp" />
<corners android:radius="5dp" />
</shape>
Next, let us create an adapter for the recyclerview
. Create a new class called RecyclerListAdapter
and paste this:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/RecyclerListAdapter.kt
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
class RecyclerListAdapter(private val listener:OnLastPositionReached): RecyclerView.Adapter<RecyclerListAdapter.ViewHolder>() {
private val contentList: ArrayList<String> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater
.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(contentList[position])
if (position == contentList.size-1){
listener.lastPositionReached()
} else {
listener.otherPosition()
}
}
override fun getItemCount(): Int = contentList.size
fun addItem(item:String) {
contentList.add(item)
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val userName: TextView = itemView.findViewById(android.R.id.text1)
fun bind(item: String) = with(itemView) {
userName.text = item
}
}
interface OnLastPositionReached {
fun lastPositionReached()
fun otherPosition()
}
}
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:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import com.pusher.client.Pusher
import com.pusher.client.PusherOptions
import kotlinx.android.synthetic.main.activity_main.*
import org.json.JSONObject
class MainActivity : AppCompatActivity(), RecyclerListAdapter.OnLastPositionReached {
private var count = 0
private val recyclerListAdapter = RecyclerListAdapter(this)
private var lastPosition = false
override fun otherPosition() {
lastPosition = false
}
override fun lastPositionReached() {
lastPosition = true
textViewNewContents.visibility = View.GONE
count = 0
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupClickListeners()
setupRecyclerView()
setupPusher()
}
}
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:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
private fun setupClickListeners() {
textViewNewContents.setOnClickListener {
recyclerViewContents.scrollToPosition(recyclerListAdapter.itemCount-1)
textViewNewContents.visibility = View.GONE
count = 0
}
}
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:
// File: ./app/src/main/java/com/example/updatingrealtimecontentpusher/MainActivity.kt
private fun setupRecyclerView() {
with(recyclerViewContents){
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = recyclerListAdapter
}
recyclerListAdapter.addItem("Hello World")
recyclerListAdapter.addItem("New article alert!")
recyclerListAdapter.addItem("Pusher is actually awesome")
recyclerListAdapter.addItem("Realtime functionality freely provided ")
recyclerListAdapter.addItem("Checkout pusher.com/tutorials")
recyclerListAdapter.addItem("You can also checkout blog.pusher.com")
recyclerListAdapter.addItem("Learn how to update contents properly ")
recyclerListAdapter.addItem("Hello World")
recyclerListAdapter.addItem("New article alert!")
recyclerListAdapter.addItem("Pusher is actually awesome")
recyclerListAdapter.addItem("Realtime functionality freely provided ")
recyclerListAdapter.addItem("Checkout pusher.com/tutorials")
recyclerListAdapter.addItem("You can also checkout blog.pusher.com")
recyclerListAdapter.addItem("Learn how to update contents properly ")
}
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:
private fun setupPusher() {
val options = PusherOptions()
options.setCluster("PUSHER_CLUSTER")
val pusher = Pusher("PUSHER_KEY", options)
val channel = pusher.subscribe("my-channel")
channel.bind("my-event") { channelName, eventName, data ->
runOnUiThread {
if (!lastPosition){
count ++
textViewNewContents.text = count.toString()
textViewNewContents.visibility = View.VISIBLE
recyclerListAdapter.addItem(JSONObject(data).getString("message"))
} else {
recyclerListAdapter.addItem(JSONObject(data).getString("message"))
recyclerViewContents.scrollToPosition(recyclerListAdapter.itemCount-1)
}
}
}
pusher.connect()
}
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
:
{
"message": "Hi there"
}
Your app should now work like this:
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.
7 December 2018
by Neo Ighodaro