You need Android Studio 3+ and Node.js installed on your machine. Some familiarity with Android development and Android Studio is required.

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:

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.

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 this, log in to your Pusher dashboard and Create a new channel app. You can do this by clicking the Create new Channels app card at the bottom right. When you create a new app, you are provided with the keys. We will need them later.

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.

Setting up Pusher Beams

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

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

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:

buildscript { dependencies { classpath 'com.google.gms:google-services:4.0.0' } }

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

dependencies { implementation 'com.android.support:appcompat-v7:28.0.0-rc01' implementation 'com.android.support:recyclerview-v7:28.0.0-rc01' implementation 'com.android.support:preference-v7:28.0.0-rc01' implementation 'com.pusher:pusher-java-client:1.8.0' implementation 'com.google.firebase:firebase-messaging:17.3.0' implementation 'com.pusher:push-notifications-android:0.10.0' } 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:

< color name = "colorPrimary" > #9E9E9E </ color > < color name = "colorPrimaryDark" > #424242 </ color > < 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:

< FrameLayout android:background = "#000" xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:id = "@+id/frame_layout" android:layout_height = "match_parent" tools:context = ".SettingsActivity" />

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

import android.os.Bundle import android.support.v7.app.AppCompatActivity class SettingsActivity : AppCompatActivity () { override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_settings) supportFragmentManager.beginTransaction() .replace(R.id.frame_layout, SettingsFragment()) .commit() } }

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:

< PreferenceScreen xmlns:android = "http://schemas.android.com/apk/res/android" > < CheckBoxPreference android:key = "amazon_preference" android:title = "Amazon" android:summary = "Receive stock updates for Amazon" android:defaultValue = "true" /> < CheckBoxPreference android:key = "apple_preference" android:title = "Apple" android:summary = "Receive stock updates for Apple" android:defaultValue = "true" /> </ 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:

import android.os.Bundle import android.support.v7.preference.PreferenceFragmentCompat class SettingsFragment : PreferenceFragmentCompat () { override fun onCreatePreferences (savedInstanceState: Bundle ?, rootKey: String ?) { setPreferencesFromResource(R.xml.preference, rootKey) } }

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:

< 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" android:background = "#000" tools:context = ".MainActivity" > < android.support.v7.widget.RecyclerView android:id = "@+id/recyclerView" android:layout_width = "match_parent" android:layout_height = "match_parent" /> </ 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:

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:

< 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 = "wrap_content" > < TextView android:layout_margin = "10dp" android:id = "@+id/stockName" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginStart = "20dp" tools:text = "Amazon" android:textAppearance = "@style/TextAppearance.AppCompat.Display1" android:textSize = "18sp" app:layout_constraintStart_toStartOf = "parent" app:layout_constraintBottom_toBottomOf = "parent" app:layout_constraintTop_toTopOf = "parent" /> < TextView android:id = "@+id/changeValue" android:layout_width = "wrap_content" android:layout_height = "wrap_content" tools:text = "+5%" app:layout_constraintTop_toTopOf = "parent" android:layout_marginEnd = "20dp" android:paddingEnd = "5dp" android:paddingStart = "5dp" app:layout_constraintBottom_toBottomOf = "parent" app:layout_constraintEnd_toEndOf = "parent" /> < TextView android:id = "@+id/currentValue" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginEnd = "10dp" tools:text = "1234.9" app:layout_constraintEnd_toStartOf = "@id/changeValue" app:layout_constraintTop_toTopOf = "parent" app:layout_constraintBottom_toBottomOf = "parent" /> </ 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:

import android.support.v4.content.res.ResourcesCompat import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView class StockListAdapter ( private val stockList:ArrayList<StockModel>) : RecyclerView.Adapter<StockListAdapter.ViewHolder>() { 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(stockList[position]) override fun getItemCount () : Int = stockList.size fun addItem (item: StockModel ) { stockList.add(item) notifyDataSetChanged() } fun updateItem (item: StockModel ) { stockList.forEachIndexed { index, element -> if (element.name == item.name){ stockList[index].changeValue = item.changeValue stockList[index].currentValue = item.currentValue notifyItemChanged(index) } } } fun contains (item: StockModel ) : Boolean { for (stock in stockList){ if (stock.name==item.name){ return true } } return false } fun removeItem (name: String ) { val it = stockList.iterator() while (it.hasNext()) { val value = it.next() if (value.name == name){ it.remove() } } notifyDataSetChanged() } inner class ViewHolder (itemView: View) : RecyclerView.ViewHolder(itemView) { private val changePercent: TextView = itemView.findViewById(R.id.changeValue) private val stockName: TextView = itemView.findViewById(R.id.stockName) private val currentValue: TextView = itemView.findViewById(R.id.currentValue) fun bind (item: StockModel ) = with(itemView) { stockName.text = item.name currentValue.text = item.currentValue.toString() val fmt = "%s%s" changePercent.text = String.format(fmt, item.changeValue.toString(), "%" ) if (item.changeValue.toString().contains( "-" )){ changePercent.background = ResourcesCompat.getDrawable(resources, android.R.color.holo_red_dark, null ) } else { changePercent.background = ResourcesCompat.getDrawable(resources, android.R.color.holo_green_dark, null ) } } } }

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:

class MyStockList { companion object { val stockList = ArrayList<StockModel>() init { stockList.add(StockModel( "Apple" , 0.0 , 0.0 )) stockList.add(StockModel( "Amazon" , 0.0 , 0.0 )) } } }

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:

import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.preference.PreferenceManager import android.support.v7.app.AppCompatActivity import android.support.v7.widget.DividerItemDecoration import android.support.v7.widget.LinearLayoutManager import android.util.Log import android.view.Menu import android.view.MenuItem 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 class MainActivity : AppCompatActivity (), SharedPreferences.OnSharedPreferenceChangeListener { private val mAdapter = StockListAdapter(ArrayList()) private lateinit var sharedPreferences: SharedPreferences private val options = PusherOptions().setCluster( "PUSHER_CLUSTER" ) private val pusher = Pusher( "PUSHER_KEY" , options) private val channel = pusher.subscribe( "stock-channel" ) override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) setupPrefs() pusher.connect() setupRecyclerView() setupPusherChannels() setupPushNotifications() } }

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.

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:

private fun setupPrefs () { PreferenceManager.setDefaultValues( this , R.xml.preference, false ) sharedPreferences = PreferenceManager.getDefaultSharedPreferences( this ) }

setupRecyclerView() - this method initializes our RecyclerView . Add the method to the class:

private fun setupRecyclerView () { with(recyclerView) { layoutManager = LinearLayoutManager( this @MainActivity ) adapter = mAdapter addItemDecoration( DividerItemDecoration(recyclerView.context, DividerItemDecoration.VERTICAL) ) } }

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:

private fun setupPusherChannels () { val sharedPref = PreferenceManager.getDefaultSharedPreferences( this ) MyStockList.stockList.forEachIndexed { index, element -> val refKey = element.name.toLowerCase() + "_preference" val refValue = sharedPref.getBoolean(refKey, false ) if (refValue) { if (!mAdapter.contains(element)) { mAdapter.addItem(element) channel.bind(element.name) { channelName, eventName, data -> val jsonObject = JSONObject( data ) runOnUiThread { mAdapter.updateItem( StockModel( eventName, jsonObject.getDouble( "currentValue" ), jsonObject.getDouble( "changePercent" ) ) ) } } } } else { mAdapter.removeItem(element.name) channel.unbind(element.name){ _, _, _ -> } } } }

setupPushNotifications() - this method initializes Pusher Beams and listens to stock interests. Add the method to the class:

private fun setupPushNotifications () { PushNotifications.start(applicationContext, "PUSHER_BEAMS_INSTANCEID" ) PushNotifications.subscribe( "stocks" ) }

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:

< menu xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:app = "http://schemas.android.com/apk/res-auto" > < item android:id = "@+id/settings" app:showAsAction = "collapseActionView" android:title = "@string/settings" /> </ menu >

Now, add these methods to your MainActivity file:

override fun onCreateOptionsMenu (menu: Menu ) : Boolean { menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected (item: MenuItem ) : Boolean { return when (item.itemId) { R.id.settings -> { startActivity(Intent( this @MainActivity ,SettingsActivity:: class . java )) true } else -> super .onOptionsItemSelected(item) } }

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:

override fun onStart () { super .onStart() sharedPreferences.registerOnSharedPreferenceChangeListener( this ) } override fun onDestroy () { super .onDestroy() sharedPreferences.unregisterOnSharedPreferenceChangeListener( this ) }

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

override fun onSharedPreferenceChanged (sharedPref: SharedPreferences ?, key: String ?) { setupPusherChannels() }

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:

import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Intent import android.os.Build import android.preference.PreferenceManager import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationManagerCompat import android.util.Log import com.google.firebase.messaging.RemoteMessage import com.pusher.pushnotifications.fcm.MessagingService class NotificationsMessagingService : MessagingService () { override fun onMessageReceived (remoteMessage: RemoteMessage ) { val sharedPref = PreferenceManager.getDefaultSharedPreferences( this ) MyStockList.stockList.forEachIndexed { index, element -> val refKey = element.name.toLowerCase() + "_preference" val refValue = sharedPref.getBoolean(refKey, false ) if (refValue && element.name == remoteMessage.notification!!.title!!){ setupNotifications(remoteMessage) } } } private fun setupNotifications (remoteMessage: RemoteMessage ) { val notificationId = 10 val channelId = "stocks" lateinit var channel:NotificationChannel 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 ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = applicationContext.getSystemService(NotificationManager:: class . java ) val name = getString(R.string.channel_name) val description = getString(R.string.channel_description) val importance = NotificationManager.IMPORTANCE_DEFAULT channel = NotificationChannel( "stock-exchange" , name, importance) channel.description = description notificationManager!!.createNotificationChannel(channel) notificationManager.notify(notificationId, mBuilder.build()) } else { val notificationManager = NotificationManagerCompat.from( this ) notificationManager.notify(notificationId, mBuilder.build()) } } }

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:

< string name = "settings" > Settings </ string > < string name = "channel_name" > Stock-Exchange </ string > < string name = "channel_description" > To receive updates about stocks </ string >

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

< application [ ... ] > // [...] < service android:name = ".NotificationsMessagingService" > < intent-filter android:priority = "1" > < action android:name = "com.google.firebase.MESSAGING_EVENT" /> </ intent-filter > </ service > </ application >

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

< manifest xmlns:android = "http://schemas.android.com/apk/res/android" package = "com.example.stockexchangeapp" > < uses-permission android:name = "android.permission.INTERNET" /> [...] </ 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:

// File: ./package.json { "name": "stockexchangeapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@pusher/push-notifications-server": "^1.0.0", "body-parser": "^1.18.3", "express": "^4.16.3", "pusher": "^2.1.2" } }

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:

module .exports = { appId : 'PUSHER_CHANNELS_APPID' , key : 'PUSHER_CHANNELS_KEY' , secret : 'PUSHER_CHANNELS_SECRET' , cluster : 'PUSHER_CHANNELS_CLUSTER' , secretKey : 'PUSHER_BEAMS_SECRET' , instanceId : 'PUSHER_BEAMS_INSTANCEID' };

Replace the placeholders above with keys from your Pusher dashboard.

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

const express = require ( 'express' ); const bodyParser = require ( 'body-parser' ); const path = require ( 'path' ); const Pusher = require ( 'pusher' ); const PushNotifications = require ( '@pusher/push-notifications-server' ); const app = express(); const pusher = new Pusher( require ( './config.js' )); const pushNotifications = new PushNotifications( require ( './config.js' )) function handleStock ( req, res, stock ) { let loopCount = 0 ; let sendToPusher = setInterval( () => { loopCount++; const changePercent = randomIntFromInterval( -10 , 10 ) const currentValue = randomIntFromInterval( 2000 , 20000 ); const stockName = (stock === 'amazon' ) ? 'Amazon' : 'Apple' const price = currentValue.toString() pusher.trigger( 'stock-channel' , stockName, {currentValue, changePercent}) pushNotifications.publish( [ 'stocks' ],{ fcm : { notification : { title : stockName, body : `The new value for ${stockName} is: ${price} ` } } }).then( ( publishResponse ) => { console .log( 'Just published:' , publishResponse.publishId); }); if (loopCount === 5 ) { clearInterval(sendToPusher) } }, 2000 ); res.json({ success : 200 }) } app.get( '/stock/amazon' , (req, res) => handleStock(req, res, 'amazon' )); app.get( '/stock/apple' , (req, res) => handleStock(req, res, 'apple' )); function randomIntFromInterval ( min,max ) { return Math .floor( Math .random()*(max-min+ 1 )+min); } const port = 5000 ; 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:

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.