Build a live blog in Kotlin with push notifications
You will need Android Studio 3+, Node and npm installed on your machine. A basic knowledge of Android development, and familiarity with Android Studio, is required.
Introduction
We are definitely in the era of realtime everything. We demand that all our tools and apps we use daily should be able to update in realtime. With a lot of realtime applications springing up, the football scene is not an exception. You see popular soccer reporting bodies adopting this strategy.
In this article, you will learn how to build a live soccer blog mobile app. Here is what your app will look like after this post:
Prerequisites
For you to move on smoothly with this tutorial, it is expected that you have the following:
- Android Studio (>= v3.0) installed on your machine. Download here.
- A basic knowledge of Android development and an ability to use Android Studio.
- A basic knowledge of Kotlin programming language. See the official docs.
- Node.js and NPM installed on your machine. Check here for the latest releases.
Building your live blog app
Creating your Android app
To get started, open Android Studio and create a new basic activity project. Android Studio provides a wizard for this to guide you. While creating your app, be sure to enable Kotlin support since that is what you will use.
Select Phone and Tablet using API 19: Android 4.4 (Kitkat). Select the EmptyActivity template and create the project. After this process is complete, you should have an activity named MainActivity
and its layout activity_main.xml
.
Setting up Pusher Channels
Log in to your Pusher dashboard. If you don’t have an account, create one. Your dashboard should look like this:
Create a new Channels app. You can easily do this by clicking the big Create new Channels app card at the bottom right. When you create a new app, you are provided with keys. Keep them safe as you will soon need them.
Getting your FCM key
Before you can start using Beams, you need an FCM key and a google-services file because Beams relies on Firebase. Go to your Firebase console and create a new project.
When you get to the console, click the Add project card to initialize the app creation wizard. Add the name of the project, for example, soccer-blog
. Read and accept the terms of conditions. 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.
An easy way to get the package name of your app is from your AndroidManifest.xml
file. Check the <manifest>
tag and copy the value of the package
attribute. Another place you can find this is your app-module build.gradle
file. Look out for the applicationId
value. When you enter the package name and click Register app. Next download your google-services.json
file. After you have downloaded the file, you can skip the rest of the process. Add the downloaded file to the app folder of your app - name-of-project/app
.
Next, go to your Firebase project settings, under the Cloud messaging tab, copy your server key.
Setting up Pusher Beams
Next, log in to the new Pusher dashboard, in here we will create a Pusher Beams instance. You should sign up if you don’t have an account yet. Click on the Beams button on the sidebar then click Create, this will launch a pop up to Create a new Beams instance. Name it soccer-blog
.
As soon as you create the instance, you will be presented with a quickstart guide. Select the ANDROID quickstart
The next screen requires the FCM key you copied earlier. After you add the FCM key, you can exit the quickstart guide.
Adding dependencies
You will make use of Beams for notifications and the Channels libraries for live events. Add the following to the project’s build-gradle
file:
// File: ./blog-app/build.gradle
buildscript {
// [...]
dependencies {
classpath 'com.google.gms:google-services:4.0.0'
}
}
And these other dependencies to the app-module build.gradle
file:
// File: ./blog-app/app/build.gradle
dependencies {
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.pusher:pusher-java-client:1.5.0'
implementation 'com.google.firebase:firebase-messaging:17.0.0'
implementation 'com.pusher:push-notifications-android:0.10.0'
}
// Add this line to the end of the file
apply plugin: 'com.google.gms.google-services'
Writing your app
Your app is expected to display a list of posts in realtime and this means you will need a list. Each list row will contain the time of action and current action happening. Since you can’t use the default Android layouts meant for lists as it doesn’t have what you require in the customized fashion you want it, we have to create a custom list row layout. This layout will determine how each item in the list will look like.
Create a new layout file, name it list_row.xml
and paste this:
<!-- File: /blog-app/app/src/main/res/layout/list_row.xml -->
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="10dp"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp">
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:padding="10dp"
android:layout_height="match_parent">
<TextView
android:textColor="@android:color/black"
android:layout_gravity="center"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"/>
<TextView
android:id="@+id/currentActivity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textSize="18sp" />
</LinearLayout>
</android.support.v7.widget.CardView>
A CardView
is here used for easy customization of the borders. The CardView
contains a horizontal linear layout, which in turn contains two TextView
s. The first TextView
will show the minute during the match when an event is happening and the second one will show the event happening.
Next, you need a class to mock the kind of data you want to send to each row. Create a data class named BlogPostModel
and paste this:
// File: /blog-app/app/src/main/java/com/example/soccerliveblog/BlogPostModel.kt
data class BlogPostModel(var time:String, var currentActivity:String)
Next, you will need a recycler view adapter to manage items in the recycler view. Create a class BlogListAdapter
and set it up like this:
// File: /blog-app/app/src/main/java/com/example/soccerliveblog/BlogListAdapter.kt
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
class BlogListAdapter : RecyclerView.Adapter<BlogListAdapter.ViewHolder>() {
private var blogList = ArrayList<BlogPostModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.list_row, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(blogList[position])
override fun getItemCount(): Int = blogList.size
fun addItem(blogItem:BlogPostModel){
blogList.add(0,blogItem)
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val time: TextView = itemView.findViewById(R.id.time)
private val currentActivity: TextView = itemView.findViewById(R.id.currentActivity)
fun bind(currentValue: BlogPostModel) = with(itemView) {
time.text = currentValue.time
currentActivity.text = currentValue.currentActivity
}
}
}
This class contains the usual RecyclerView.Adapter
methods. There are two custom functions created here, addItem
to add a new blog post item to the top of the list and bind
inside the ViewHolder
class to make binding easier.
Next thing you would consider is how you will receive and display notifications in the app. You will create a service for that. Services are one of the key components in Android development. A service is a component that runs in the background to perform long-running operations without needing to interact with the user and it works even if application is destroyed. Create a new class named NotificationsMessagingService
and paste this:
// File: /blog-app/app/src/main/java/com/example/soccerliveblog/NotificationsMessagingService.kt
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.support.v4.app.NotificationCompat
import com.google.firebase.messaging.RemoteMessage
import com.pusher.pushnotifications.fcm.MessagingService
class NotificationsMessagingService : MessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val notificationId = 10
val channelId = "soccer-channel"
val notificationManager = applicationContext.getSystemService(NotificationManager::class.java)
lateinit var channel:NotificationChannel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = getString(R.string.channel_name)
val description = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
channel = NotificationChannel("world-cup", name, importance)
channel.description = description
notificationManager!!.createNotificationChannel(channel)
}
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val mBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(remoteMessage.notification!!.title!!)
.setContentText(remoteMessage.notification!!.body!!)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
notificationManager.notify(notificationId, mBuilder.build())
}
}
This class implements the MessagingService
abstract class. This mandates the implementation of the method onMessageReceived
, which is called when a message is pushed remotely. From the snippet, when a message is received, a notification is prepared and sent to the user.
Note that if the user is on the app already, this notification will not come up. API versions 26 and above require creating notification channels and that is exactly what is done above. The title and body of the notification are inline with what is received remotely. An intent is added so that the MainActivity
will be opened when the notification is selected.
Add these strings to your strings.xml
file as they were referenced in the previous snippet:
<string name="channel_name">soccer</string>
<string name="channel_description">Listen to soccer notifications</string>
Next, you add the service in your AndroidManifest.xml
file under the <application>
tag like this:
<!-- /blog-app/app/src/main/AndroidManifest.xml -->
<application
[...]
<service android:name=".NotificationsMessagingService">
<intent-filter android:priority="1">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
[...]
</application>
With this, you are ready to receive notifications remotely to your app. Next replace the contents of the activity_main.xml
file with the following:
<!-- File: /blog-app/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">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerViewBlogPosts"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
Now, go the MainActivity
class and make sure you have these imports in the class:
// File: /blog-app/app/src/main/java/com/example/soccerliveblog/MainActivity.kt
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import com.pusher.client.Pusher
import com.pusher.client.PusherOptions
import com.pusher.pushnotifications.PushNotifications
import kotlinx.android.synthetic.main.activity_main.*
import org.json.JSONObject
The rest of the class should then look like this:
class MainActivity : AppCompatActivity() {
private lateinit var pusher: Pusher
private val blogListAdapter = BlogListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
PushNotifications.start(applicationContext,
PUSHER_BEAMS_INSTANCEID)
PushNotifications.subscribe("world-cup")
with(recyclerViewBlogPosts){
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = blogListAdapter
}
setupPusher()
}
}
You are expected to replace the
PUSHER_BEAMS_INSTANCEID
with your instance ID found on your Pusher Beams dashboard.
In the above snippet, the pusher
and blogListAdapter
instance are first declared. In the onCreate
method, push notifications is initialized and you are particularly listening to the world-cup stream for updates.
Next, you have initialized the recycler view with a linear layout manager and an adapter. Finally, a setupPusher
function is called. Add the function below to the class:
private fun setupPusher() {
val options = PusherOptions()
options.setCluster(PUSHER_CLUSTER)
pusher = Pusher(PUSHER_API_KEY, options)
val channel = pusher.subscribe("soccer")
channel.bind("world-cup") { channelName, eventName, data ->
val jsonObject = JSONObject(data)
val time = jsonObject.getString("currentTime")
val currentActivity = jsonObject.getString("currentPost")
val model = BlogPostModel(time,currentActivity)
runOnUiThread {
blogListAdapter.addItem(model)
}
}
pusher.connect()
}
Replace the
PUSHER_CLUSTER
andPUSHER_API_KEY
with their equivalent values from your dashboard
In the above snippet, there is a listener to the soccer channel and the world-cup event. When a post is received, it is bound to a new model instance before it being added to the adapter.
Finally, give the activity a singleInstance
launch mode so that when you open the MainActivity
from the notification, it won’t restart the activity. To do this, open the AndroidManifest.xml
file and add the android:launchMode
attribute to the activity
tag and set it to singleInstance
:
<activity android:name=".MainActivity"
android:launchMode="singleInstance"
...
</activity>
Building the backend
Let’s build a simple Node.js server to power our app. Create a new folder say beams-backend
. Open the folder. Create a new config.js
file like this:
module.exports = {
appId: 'PUSHER_APP_ID',
key: 'PUSHER_APP_KEY',
secret: 'PUSHER_APP_SECRET',
cluster: 'PUSHER_APP_CLUSTER',
secretKey: 'PUSHER_BEAMS_SECRET',
instanceId: 'PUSHER_BEAMS_INSTANCEID'
};
Replace the first four items with the keys on you Pusher Channel dashboard while the last two keys will be replaced with the keys on your Pusher Beams dashboard
This file holds the keys you will access. It is good practice to keep them all in one file. Next up, create a another file named index.js
and paste this:
// Load the required libraries
let Pusher = require('pusher');
let express = require('express');
let bodyParser = require('body-parser');
const PushNotifications = require('@pusher/push-notifications-server');
// initialize express and pusher and pusher beams
let app = express();
let pusher = new Pusher(require('./config.js'));
let pushNotifications = new PushNotifications(require('./config.js'))
// Middlewares
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.get('/posts', (req, res) => {
var arrayEvents = [
"Russia (4-2-3-1): Igor Akinfeev; Mario Fernandes, Ilya Kutepov, Sergey Ignashevich, Yury Zhirkov; Yuri Gazinskiy, Roman Zobnin; Aleksandr Samedov, Alan Dzagoev, Aleksandr Golovin; Fedor Smolov.",
"Finally, the festival of football is here. We've got 64 games, 32 teams...but there can be only one winner. And the action starts today!!",
"Hello and welcome to live text commentary of the Group A match between Russia and Saudi Arabia at the 2018 World Cup in Russia. The scene is set for the tournament opener!"
];
var arrayTime = ["15'", "10'", "5'"];
let sendPushNotification = () => {
var currentPost = arrayEvents.pop()
var currentTime = arrayTime.pop()
pushNotifications.publish(
['world-cup'],{
fcm: {
notification: {
title: 'New post',
body: currentPost
}
}
}).then((publishResponse) => {
console.log('Just published:', publishResponse.publishId);
});
pusher.trigger('soccer', 'world-cup', {currentTime, currentPost});
}
sendPushNotification()
let sendToPusher = setInterval(() => {
sendPushNotification()
if (arrayEvents.length == 0) {
clearInterval(sendToPusher)
}
}, 5000);
res.json({success: 200})
});
// index
app.get('/', (req, res) => res.json("It works!"));
// serve app
app.listen(4000, _ => console.log('App listening on port 4000!'));
These commentaries were gotten manually from Goal.com’s commentary blog for the opening match at the FIFA World Cup 2018 (Russia vs Saudi Arabia)
In this snippet, there is one endpoint, the /posts
endpoint, which sends data to the Pusher channel and a notification channel every twenty seconds. The data is gotten from an array initialized locally. The app is then served on port 4000
. In the beams-backend
directory, run the following commands:
npm install pusher
npm install body-parser
npm install @pusher/push-notifications-server express --save
This installs the three dependencies you need for your app. Now, your server is ready, run this command in the beams-backend
directory:
node index.js
Next, run the application from Android Studio, then open the URL http://localhost:4000/posts
in your browser and you should start receiving live updates in your app.
Conclusion
In this post, you have learned how to build a blog app using Kotlin, Pusher Beams, and Pusher Channels. Feel free to fall back to the GitHub repository if you get stuck at any point. I can’t wait to see what you will build with your knowledge of a realtime service like Pusher.
1 July 2018
by Neo Ighodaro