Create a stocks application for Android featuring push notifications

Introduction

A stock market, equity market or share market is the aggregation of buyers and sellers (a loose network of economic transactions, not a physical facility or discrete entity) of stocks (also called shares), which represent ownership claims on businesses; these may include securities listed on a public stock exchange as well as those only traded privately. - Wikipedia

Building a stock market application has some fundamental requirements. Apart from accuracy, the application needs to also be able to update prices in realtime as the changes occur. It should also have an option to notify the user if there is a change in the price of a stock, even when they are not currently looking at the application.

In this post, we will see how we can achieve this using Kotlin, Pusher Beams, and Pusher Channels.

When we are done with the article, here is what our app will look like:

android-stocks-demo

Prerequesites

To follow along in this tutorial, you need the following:

  • Android Studio (v3.x or later) installed on your machine. Download here.
  • Knowledge of Android development and the Android Studio IDE.
  • Knowledge of Kotlin. Visit the docs.
  • Node.js and NPM installed on your machine. Download any stable release here.
  • Pusher account. Create a free sandbox Pusher account or sign in.

Creating your Android application

The first thing we want to do is create our Android application. Open Android Studio and create a new application.

Enter the name of your application, StockExchangeApp, and then enter the package name, which is com.example.stockexchangeapp. Make sure the Enable Kotlin Support checkbox is selected, choose the minimum SDK, we chose API 19, click Next. Choose an Empty Activity template and click Finish.

Setting up Pusher Channels

For this article, you need a Pusher application. To get started, go to the Pusher dashboard and create a new Channels app. When you create a new app, you are provided with the keys. We will need them later. For more details, visit the Pusher Channels docs.

Getting your FCM key

For Pusher Beams to work, we need an FCM key and a google-services.json file. Go to your Firebase console and click the Add project card to initialize the app creation wizard. Add the name of project, read and accept the terms and 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. You can find your app’s package name is your app-module build.gradle file. Look for the applicationId value. When you enter the package name and click Next, you will be prompted to download a google-services.json file. Download the file and skip the rest of the process. Add the downloaded file to the app folder of your project - StockExchangeApp/app.

To get the FCM key, go to your project settings on Firebase, under the Cloud messaging tab, copy out the server key.

android-stocks-firebase-cloud-messaging

Go get additional details, visit Pusher Beams docs.

Setting up Pusher Beams

Now that we have set up our Firebase application, go to the Pusher dashboard and click on the CREATE button on the BEAMS card.

android-stocks-new-beams

After creating the instance, you will be presented with a quickstart guide. Select the ANDROID quickstart.

android-stocks-beams-android

The next screen requires the FCM key which you copied earlier. After you add the FCM key, you can exit the guide.

Adding application dependencies

Since this application will depend on other libraries to function, let’s pull in these dependencies so they are available to the project.

Open the project build.gradle file and add the add the following:

1// File: ./build.gradle
2    buildscript {
3        // [...]
4    
5        dependencies {
6            // [...]
7            
8            classpath 'com.google.gms:google-services:4.0.0'
9        }
10    }

And these other dependencies to the app-module build.gradle file:

1// File: ./app/build.gradle
2    dependencies {
3    
4        // [...]
5    
6        // Support library
7        implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
8        implementation 'com.android.support:recyclerview-v7:28.0.0-rc01'
9        implementation 'com.android.support:preference-v7:28.0.0-rc01'
10    
11        // Pusher Channel
12        implementation 'com.pusher:pusher-java-client:1.8.0'
13    
14        // Pusher Beams
15        implementation 'com.google.firebase:firebase-messaging:17.3.0'
16        implementation 'com.pusher:push-notifications-android:0.10.0'
17    }
18    
19    // Add this line to the end of the file
20    apply plugin: 'com.google.gms.google-services'

Building the application

So far, we have been setting up the project. Now let’s start building the application. Let’s start by tweaking the colors of the application.

Open the colors.xml file and add the following code to it:

1<!-- File: ./app/src/main/res/values/colors.xml -->
2    <color name="colorPrimary">#9E9E9E</color>
3    <color name="colorPrimaryDark">#424242</color>
4    <color name="colorAccent">#607D8B</color>

Next, open your styles.xml file and replace the parent theme on the app theme with this - Theme.AppCompat.

Apart from the initial MainActivity already created for us, we will need a screen to manage the settings for the application.

Create a new Empty Activity named SettingsActivity. Open the layout created for it - activity_settings and replace everything except the first line of the file with the following code:

1<!-- File: ./app/src/main/res/layout/activity_settings.xml -->
2    <FrameLayout
3      android:background="#000"
4      xmlns:android="http://schemas.android.com/apk/res/android"
5      xmlns:tools="http://schemas.android.com/tools"
6      android:layout_width="match_parent"
7      android:id="@+id/frame_layout"
8      android:layout_height="match_parent"
9      tools:context=".SettingsActivity"/>

Next, open the SettingsActivity file and set it up like this:

1// File: ./app/src/main/java/com/example/stockexchangeapp/SettingsActivity.kt
2    // [...]
3    
4    import android.os.Bundle
5    import android.support.v7.app.AppCompatActivity
6    
7    class SettingsActivity: AppCompatActivity() {
8    
9      override fun onCreate(savedInstanceState: Bundle?) {
10        super.onCreate(savedInstanceState)
11        setContentView(R.layout.activity_settings)
12        supportFragmentManager.beginTransaction()
13            .replace(R.id.frame_layout, SettingsFragment())
14            .commit()
15      }
16    
17    }

In the code above, we replaced the frame layout with a fragment. This is the recommended practice when creating a settings page. Before creating the fragment, let’s create a preference file.

Create a new file in the xml directory named preference and paste the following:

1<!-- File: ./app/src/main/res/xml/preference.xml -->
2    <PreferenceScreen
3      xmlns:android="http://schemas.android.com/apk/res/android">
4    
5      <CheckBoxPreference
6        android:key="amazon_preference"
7        android:title="Amazon"
8        android:summary="Receive stock updates for Amazon"
9        android:defaultValue="true" />
10    
11      <CheckBoxPreference
12        android:key="apple_preference"
13        android:title="Apple"
14        android:summary="Receive stock updates for Apple"
15        android:defaultValue="true" />
16    
17    </PreferenceScreen>

In this file, we have two checkboxes to control the updates for two stocks, Amazon and Apple.

Next, create a new class named SettingsFragment and paste the following code:

1// File: ./app/src/main/java/com/example/stockexchangeapp/SettingsFragment.kt
2    // [...]
3    
4    import android.os.Bundle
5    import android.support.v7.preference.PreferenceFragmentCompat
6    
7    class SettingsFragment: PreferenceFragmentCompat() {
8    
9      override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
10        // Load the preferences from an XML resource
11        setPreferencesFromResource(R.xml.preference, rootKey)
12      }
13    
14    }

The code above loads the settings from the preference file created earlier. With this, we are done implementing the settings screen.

The next screen to be added will be a list of stock prices, which will be shown in the MainActivity. To do this, we need a list. Open the activity_main.xml file and paste the following:

1<!-- File: ./app/src/main/res/layout/activity_main.xml -->
2    <android.support.constraint.ConstraintLayout
3      xmlns:android="http://schemas.android.com/apk/res/android"
4      xmlns:app="http://schemas.android.com/apk/res-auto"
5      xmlns:tools="http://schemas.android.com/tools"
6      android:layout_width="match_parent"
7      android:layout_height="match_parent"
8      android:background="#000"
9      tools:context=".MainActivity">
10    
11      <android.support.v7.widget.RecyclerView
12        android:id="@+id/recyclerView"
13        android:layout_width="match_parent"
14        android:layout_height="match_parent"/>
15    
16    </android.support.constraint.ConstraintLayout>

This layout has a ConstraintLayout housing a RecyclerView. Since we are using a list, we need some other utilities. One of those utilities is a data object. The object is what every item on the list will hold.

Related: Getting started with ConstraintLayout in Kotlin.

Create a new class named StockModel and paste this:

1// File: ./app/src/main/java/com/example/stockexchangeapp/StockModel.kt
2    data class StockModel(var name: String, var currentValue: Double, var changeValue:Double)

A data class in Kotlin generates some other useful methods we would have had to create manually if we were using Java.

Next, let’s design a layout for how each list item will look. Create a new layout file named list_row and paste this:

1<!-- File: ./app/src/main/res/layout/list_row.xml -->
2    <android.support.constraint.ConstraintLayout 
3      xmlns:android="http://schemas.android.com/apk/res/android"
4      xmlns:app="http://schemas.android.com/apk/res-auto"
5      xmlns:tools="http://schemas.android.com/tools"
6      android:layout_width="match_parent"
7      android:layout_height="wrap_content">
8    
9      <TextView
10        android:layout_margin="10dp"
11        android:id="@+id/stockName"
12        android:layout_width="wrap_content"
13        android:layout_height="wrap_content"
14        android:layout_marginStart="20dp"
15        tools:text="Amazon"
16        android:textAppearance="@style/TextAppearance.AppCompat.Display1"
17        android:textSize="18sp"
18        app:layout_constraintStart_toStartOf="parent"
19        app:layout_constraintBottom_toBottomOf="parent"
20        app:layout_constraintTop_toTopOf="parent" />
21    
22      <TextView
23        android:id="@+id/changeValue"
24        android:layout_width="wrap_content"
25        android:layout_height="wrap_content"
26        tools:text="+5%"
27        app:layout_constraintTop_toTopOf="parent"
28        android:layout_marginEnd="20dp"
29        android:paddingEnd="5dp"
30        android:paddingStart="5dp"
31        app:layout_constraintBottom_toBottomOf="parent"
32        app:layout_constraintEnd_toEndOf="parent" />
33    
34      <TextView
35        android:id="@+id/currentValue"
36        android:layout_width="wrap_content"
37        android:layout_height="wrap_content"
38        android:layout_marginEnd="10dp"
39        tools:text="1234.9"
40        app:layout_constraintEnd_toStartOf="@id/changeValue"
41        app:layout_constraintTop_toTopOf="parent"
42        app:layout_constraintBottom_toBottomOf="parent" />
43    
44    </android.support.constraint.ConstraintLayout>

From this layout, each list item will show a company name, the current stock price, and it’ll show the change percentage.

Next, let’s create the adapter for the list. Create a new class named StockListAdapter and paste this:

1// File: ./app/src/main/java/com/example/stockexchangeapp/StockListAdapter.kt
2    // [...]
3    
4    import android.support.v4.content.res.ResourcesCompat
5    import android.support.v7.widget.RecyclerView
6    import android.view.LayoutInflater
7    import android.view.View
8    import android.view.ViewGroup
9    import android.widget.TextView
10    
11    class StockListAdapter(private val stockList:ArrayList<StockModel>) : RecyclerView.Adapter<StockListAdapter.ViewHolder>() {
12    
13      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
14        return ViewHolder(LayoutInflater.from(parent.context)
15            .inflate(R.layout.list_row, parent, false))
16      }
17    
18      override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(stockList[position])
19    
20      override fun getItemCount(): Int = stockList.size
21    
22      fun addItem(item:StockModel){
23        stockList.add(item)
24        notifyDataSetChanged()
25      }
26    
27      fun updateItem(item:StockModel) {
28        stockList.forEachIndexed { index, element ->
29          if (element.name == item.name){
30            stockList[index].changeValue = item.changeValue
31            stockList[index].currentValue = item.currentValue
32            notifyItemChanged(index)
33          }
34        }
35      }
36    
37      fun contains(item: StockModel):Boolean{
38        for (stock in stockList){
39          if (stock.name==item.name){
40            return true
41          }
42        }
43    
44        return false
45      }
46    
47      fun removeItem(name: String) {
48        val it = stockList.iterator()
49    
50        while (it.hasNext()) {
51          val value = it.next()
52          if (value.name == name){
53            it.remove()
54          }
55        }
56        
57        notifyDataSetChanged()
58      }
59    
60      inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
61        private val changePercent: TextView = itemView.findViewById(R.id.changeValue)
62        private val stockName: TextView = itemView.findViewById(R.id.stockName)
63        private val currentValue: TextView = itemView.findViewById(R.id.currentValue)
64    
65        fun bind(item: StockModel) = with(itemView) {
66          stockName.text = item.name
67          currentValue.text = item.currentValue.toString()
68          val fmt = "%s%s"
69    
70          changePercent.text = String.format(fmt, item.changeValue.toString(), "%")
71    
72          if (item.changeValue.toString().contains("-")){
73            changePercent.background = ResourcesCompat.getDrawable(resources, android.R.color.holo_red_dark, null)
74          } else {
75            changePercent.background = ResourcesCompat.getDrawable(resources, android.R.color.holo_green_dark, null)
76          }
77        }
78      }
79    }

This class manages the display of stock items on the list. It collects an initial list passed from the constructor and uses the size of that to know how many items we have.

The list can be updated with the additem, updateItem, and removeItem methods we created. The list_row layout we designed earlier is used in the onCreateViewHolder method. In the bind method of the ViewHolder class, apart from adding the values to the necessary text views, we apply a green or red background to the changePercent text view if it is a positive or negative value.

For uniformity, we will create a new class that will hold the list items we will use throughout the client app. Create a new class named MyStockList and paste this:

1// File: app/src/main/java/com/example/stockexchangeapp/MyStockList.kt
2    // [...]
3    
4    class MyStockList{
5      companion object {
6        val stockList = ArrayList<StockModel>()
7        init {
8          stockList.add(StockModel("Apple", 0.0, 0.0))
9          stockList.add(StockModel("Amazon", 0.0, 0.0))
10        }
11      }
12    }

For this demo, we are considering two stocks only. You can add more if you like. These stocks have a default value of 0.0 for change percent and value.

Next, we will add some logic to our MainActivity file. Open the file and paste the following:

1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    // [...]
3    
4    import android.content.Intent
5    import android.content.SharedPreferences
6    import android.os.Bundle
7    import android.preference.PreferenceManager
8    import android.support.v7.app.AppCompatActivity
9    import android.support.v7.widget.DividerItemDecoration
10    import android.support.v7.widget.LinearLayoutManager
11    import android.util.Log
12    import android.view.Menu
13    import android.view.MenuItem
14    import com.pusher.client.Pusher
15    import com.pusher.client.PusherOptions
16    import com.pusher.pushnotifications.PushNotifications
17    import kotlinx.android.synthetic.main.activity_main.*
18    import org.json.JSONObject
19    
20    class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
21    
22      private val mAdapter = StockListAdapter(ArrayList())
23      private lateinit var sharedPreferences: SharedPreferences
24      private val options = PusherOptions().setCluster("PUSHER_CLUSTER")
25      private val pusher = Pusher("PUSHER_KEY", options)
26      private val channel = pusher.subscribe("stock-channel")
27    
28      override fun onCreate(savedInstanceState: Bundle?) {
29        super.onCreate(savedInstanceState)
30        setContentView(R.layout.activity_main)
31        setupPrefs()
32        pusher.connect()
33        setupRecyclerView()
34        setupPusherChannels()
35        setupPushNotifications()
36      }
37    
38    }

This class implements the SharedPreferences.OnSharedPreferenceChangeListener interface because we will add settings functionality in the app and the callback will tell us when the settings have been updated.

We created instance variables for our Pusher Channel object and the list adapter. We subscribed to the stock-channel channel to listen for stock updates.

NOTE: Replace the Pusher holders with the keys on your Pusher Channel dashboard

Other methods called in the onCreate method include:

  • setupPrefs() - this method initializes the sharedPreferences variable and initializes our settings with the default values. Add the method to the class:
1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    private fun setupPrefs() {
3      PreferenceManager.setDefaultValues(this, R.xml.preference, false)
4      sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
5    }
  • setupRecyclerView() - this method initializes our RecyclerView. Add the method to the class:
1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    private fun setupRecyclerView() {
3      with(recyclerView) {
4        layoutManager = LinearLayoutManager(this@MainActivity)
5        adapter = mAdapter
6        addItemDecoration(
7            DividerItemDecoration(recyclerView.context, DividerItemDecoration.VERTICAL)
8        )
9      }
10    }
  • setupPusherChannels() - this method loops through the stock list and looks for the stocks enabled in our settings page. If any of the stock is enabled, we subscribe to receive updates. Add the method to the class:
1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    private fun setupPusherChannels(){
3        val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
4        MyStockList.stockList.forEachIndexed { index, element ->
5            val refKey = element.name.toLowerCase() + "_preference"
6            val refValue = sharedPref.getBoolean(refKey, false)
7            if (refValue) {
8                if (!mAdapter.contains(element)) {
9                    mAdapter.addItem(element)
10                    channel.bind(element.name) { channelName, eventName, data ->
11                        val jsonObject = JSONObject(data)
12                        runOnUiThread {
13                            mAdapter.updateItem(
14                                StockModel(
15                                    eventName, 
16                                    jsonObject.getDouble("currentValue"), 
17                                    jsonObject.getDouble("changePercent")
18                                )
19                            )
20                        }
21                    }
22                }
23            } else {
24                mAdapter.removeItem(element.name)
25                channel.unbind(element.name){ _, _, _ -> }
26            }
27        }
28    }
  • setupPushNotifications() - this method initializes Pusher Beams and listens to stock interests. Add the method to the class:
1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    private fun setupPushNotifications() {
3        PushNotifications.start(applicationContext, "PUSHER_BEAMS_INSTANCEID")
4        PushNotifications.subscribe("stocks")
5    }

NOTE: Replace the PUSHER_BEAMS_INSTANCEID placeholder with the Pusher Beams instance ID on your dashboard.

Remember we created a settings page earlier? Let’s inflate our menu and link it to the settings page. To do this, first, let’s first create a menu file in the menu resource folder.

Create a new menu file named menu_main.xml and paste this:

1<!-- File: app/src/main/res/menu/menu_main.xml -->
2    <menu xmlns:android="http://schemas.android.com/apk/res/android"
3      xmlns:app="http://schemas.android.com/apk/res-auto">
4    
5      <item
6        android:id="@+id/settings"
7        app:showAsAction="collapseActionView"
8        android:title="@string/settings" />
9    
10    </menu>

Now, add these methods to your MainActivity file:

1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    override fun onCreateOptionsMenu(menu: Menu): Boolean {
3      menuInflater.inflate(R.menu.menu_main, menu)
4      return true
5    }
6    
7    override fun onOptionsItemSelected(item: MenuItem): Boolean {
8      return when (item.itemId) {
9        R.id.settings -> {
10          startActivity(Intent(this@MainActivity,SettingsActivity::class.java))
11          true
12        }
13        else -> super.onOptionsItemSelected(item)
14      }
15    }

These methods add the menu to the toolbar of our main application screen and add an action when settings is selected. We then register and unregister the listener in the appropriate callback methods like so:

1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    
3    override fun onStart() {
4      super.onStart()
5      sharedPreferences.registerOnSharedPreferenceChangeListener(this)
6    }
7    
8    override fun onDestroy() {
9      super.onDestroy()
10      sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
11    }

Finally, let’s add the callback for the shared preference listener:

1// file: app/src/main/java/com/example/stockexchangeapp/MainActivity.kt
2    override fun onSharedPreferenceChanged(sharedPref: SharedPreferences?, key: String?) {
3      setupPusherChannels()
4    }

When the settings change, we call the setupPusherChannels method again so it binds to the stock reports we enabled and unbind from those we disabled.

To complete our Pusher Beams setup, we need a service that will handle incoming notifications. Create a new class named NotificationsMessagingService and paste this:

1// file: app/src/main/java/com/example/stockexchangeapp/NotificationsMessagingService.kt
2    // [...]
3    
4    import android.app.NotificationChannel
5    import android.app.NotificationManager
6    import android.app.PendingIntent
7    import android.content.Intent
8    import android.os.Build
9    import android.preference.PreferenceManager
10    import android.support.v4.app.NotificationCompat
11    import android.support.v4.app.NotificationManagerCompat
12    import android.util.Log
13    import com.google.firebase.messaging.RemoteMessage
14    import com.pusher.pushnotifications.fcm.MessagingService
15    
16    class NotificationsMessagingService : MessagingService() {
17    
18      override fun onMessageReceived(remoteMessage: RemoteMessage) {
19        val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
20        MyStockList.stockList.forEachIndexed { index, element ->
21          val refKey = element.name.toLowerCase() + "_preference"
22          val refValue = sharedPref.getBoolean(refKey, false)
23          if (refValue && element.name == remoteMessage.notification!!.title!!){
24            setupNotifications(remoteMessage)
25          }
26        }
27      }
28    
29      private fun setupNotifications(remoteMessage: RemoteMessage) {
30        val notificationId = 10
31        val channelId  = "stocks"
32        lateinit var channel:NotificationChannel
33        val intent = Intent(this, MainActivity::class.java)
34        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
35        val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
36        val mBuilder = NotificationCompat.Builder(this, channelId)
37            .setSmallIcon(R.mipmap.ic_launcher)
38            .setContentTitle(remoteMessage.notification!!.title!!)
39            .setContentText(remoteMessage.notification!!.body!!)
40            .setContentIntent(pendingIntent)
41            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
42            .setAutoCancel(true)
43    
44        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
45          val notificationManager = applicationContext.getSystemService(NotificationManager::class.java)
46          val name = getString(R.string.channel_name)
47          val description = getString(R.string.channel_description)
48          val importance = NotificationManager.IMPORTANCE_DEFAULT
49          channel = NotificationChannel("stock-exchange", name, importance)
50          channel.description = description
51          notificationManager!!.createNotificationChannel(channel)
52          notificationManager.notify(notificationId, mBuilder.build())
53    
54        } else {
55          val notificationManager =  NotificationManagerCompat.from(this)
56          notificationManager.notify(notificationId, mBuilder.build())
57        }
58      }
59    
60    }

In the code above, when a new message comes, we check if we enabled price reporting for that stock. With this information, we know whether to display the notification for it or not.

Next, open the strings.xml file and add the following to the file:

1<!-- File: app/src/main/res/values/strings.xml -->
2    <string name="settings">Settings</string>
3    <string name="channel_name">Stock-Exchange</string>
4    <string name="channel_description">To receive updates about stocks</string>

Next, open the AndroidManifest.xml file and update as seen below:

1<!-- File: ./app/main/AndroidManifest.xml -->
2    
3    <application
4      [...]
5      >
6    
7      // [...]
8    
9      <service android:name=".NotificationsMessagingService">
10        <intent-filter android:priority="1">
11          <action android:name="com.google.firebase.MESSAGING_EVENT" />
12        </intent-filter>
13      </service>
14      
15    </application>

In the AndroidManifest.xml file, add the internet permission as seen below:

1<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2      package="com.example.stockexchangeapp">
3    
4      <uses-permission android:name="android.permission.INTERNET"/>
5    
6      [...]
7    
8    </manifest>

With this, our Android application is complete. Let us now build our backend server.

Building our backend server

Now that we have completed building the application, let us build the backend of the application. We will build our backend with Node.js.

Create a new folder for your project. Navigate into the folder and create a new package.json file, then paste the following code:

1// File: ./package.json
2    {
3      "name": "stockexchangeapp",
4      "version": "1.0.0",
5      "description": "",
6      "main": "index.js",
7      "scripts": {
8        "test": "echo \"Error: no test specified\" && exit 1"
9      },
10      "keywords": [],
11      "author": "",
12      "license": "ISC",
13      "dependencies": {
14        "@pusher/push-notifications-server": "^1.0.0",
15        "body-parser": "^1.18.3",
16        "express": "^4.16.3",
17        "pusher": "^2.1.2"
18      }
19    }

This file contains the meta data for the Node application. It also contains the list of dependencies the application relies on to function properly.

Next, let’s create a configuration file that will hold all our sensitive keys. Create a file named config.js and paste this:

1// File: ./config.js
2    module.exports = {
3        appId: 'PUSHER_CHANNELS_APPID',
4        key: 'PUSHER_CHANNELS_KEY',
5        secret: 'PUSHER_CHANNELS_SECRET',
6        cluster: 'PUSHER_CHANNELS_CLUSTER',
7        secretKey: 'PUSHER_BEAMS_SECRET',
8        instanceId: 'PUSHER_BEAMS_INSTANCEID'
9    };

NOTE: Replace the placeholders above with keys from your Pusher dashboard.

Finally, let’s create another file named index.js and paste this:

1// File: ./index.js
2    
3    // import dependencies
4    const express = require('express');
5    const bodyParser = require('body-parser');
6    const path = require('path');
7    const Pusher = require('pusher');
8    const PushNotifications = require('@pusher/push-notifications-server');
9    
10    // initialise express
11    const app = express();
12    const pusher = new Pusher(require('./config.js'));
13    const pushNotifications = new PushNotifications(require('./config.js'))
14    
15    function handleStock(req, res, stock) {
16        let loopCount = 0;
17        
18        let sendToPusher = setInterval(() => {
19            loopCount++;
20            const changePercent = randomIntFromInterval(-10, 10)
21            const currentValue  = randomIntFromInterval(2000, 20000);        
22            const stockName = (stock === 'amazon') ? 'Amazon' : 'Apple'
23            const price = currentValue.toString()
24    
25            // Send to pusher
26            pusher.trigger('stock-channel', stockName, {currentValue, changePercent})
27    
28            pushNotifications.publish(
29                ['stocks'],{
30                fcm: {
31                  notification: {
32                    title: stockName,
33                    body: `The new value for ${stockName} is: ${price}`
34                  }
35                }
36              }).then((publishResponse) => {
37                console.log('Just published:', publishResponse.publishId);
38              });
39    
40            if (loopCount === 5) {
41              clearInterval(sendToPusher)
42            }
43        }, 2000);
44        
45        res.json({success: 200})
46    }
47    
48    app.get('/stock/amazon', (req, res) => handleStock(req, res, 'amazon'));
49    app.get('/stock/apple', (req, res) => handleStock(req, res, 'apple'));
50    
51    function randomIntFromInterval(min,max) {
52        return Math.floor(Math.random()*(max-min+1)+min);
53    }
54    
55    const port = 5000;
56    
57    app.listen(port, () => console.log(`Server is running on port ${port}`));

This code above contains the endpoints for our application. We have two endpoints, one to handle all the processes for the amazon stock, and the other for the apple stock. We have the handleStock method that basically does all the work.

In the folder directory, run this to install the modules:

    $ npm install

Then run the following code to start the application:

    $ node index.js

Now, if you run your app, you should see something like this:

android-stocks-demo

Conclusion

In this post, we have learned how to leverage the power of Pusher to create powerful engaging applications.

You can find the source code on GitHub. Feel free to clone and explore further.