Build a realtime graph in Android
A basic understanding of Java and Node.js are needed to follow this tutorial.
Despite the hype of serverless architectures and microservices, there are still a lot of applications deployed in servers that need to be managed, and one important part of this task is monitoring resources like CPU, memory, or disk space.
There are a lot of commercial and open source tools for monitoring servers, but what if you just need something simple and specific? Maybe something that can easily show in realtime if things are doing fine, and that you can check on your phone.
In this tutorial, we’ll set up a Node.js process to calculate the memory usage of the system at specified intervals, send this information to a Pusher channel, and show it as a graph in an Android app.
This is how the final app will look:
Setting up your Pusher application
Create a free account at https://pusher.com/signup.
When you create an app, you’ll be asked to enter some configuration options:
Enter a name, choose Android as your front-end tech, and Node.js as the back-end tech. This will give you some sample code to get you started:
But don’t worry, this won’t lock you into this specific set of technologies as you can always change them. With Pusher, you can use any combination of libraries.
Next, copy your cluster ID (next to the app title, in this example mt1
), App ID, Key, and Secret information, we’ll need them next. You can also find them in the App Keys tab.
The Node process
In Node.js, the os module provides a number of operating system-related utility methods.
After requiring the module:
const os = require('os');
We can use the totalmem()
function to get the total amount of system memory in bytes and freemem()
to get the amount of free system memory, also in bytes.
This way, we use the setInterval
function to get the memory information every ten seconds, for example, calculate the used memory, and publish it to a Pusher channel:
const os = require('os');
const Pusher = require('pusher');
// Set up Pusher
const pusher = new Pusher({
appId: '<INSERT_PUSHER_APP_ID>',
key: '<INSERT_PUSHER_APP_KEY>',
secret: '<INSERT_PUSHER_APP_SECRET>',
cluster: '<INSERT_PUSHER_APP_CLUSTER>',
encrypted: true,
});
// To convert from bytes to gigabytes
const bytesToGigaBytes = 1024 * 1024 * 1024;
// To specify the interval (in milliseconds)
const intervalInMs = 10000;
setInterval(() => {
const totalMemGb = os.totalmem()/bytesToGigaBytes;
const freeMemGb = os.freemem()/bytesToGigaBytes;
const usedMemGb = totalMemGb - freeMemGb;
console.log(`Total: ${totalMemGb}`);
console.log(`Free: ${freeMemGb}`);
console.log(`Used: ${usedMemGb}`);
// To publish to the channel 'stats' the event 'new_memory_stat'
pusher.trigger('stats', 'new_memory_stat', {
memory: usedMemGb,
});
}, intervalInMs);
Save this to a file, for example memory.js
, create a package.json
file if you haven’t already with:
npm init -y
Install the Pusher dependency with:
npm install --save pusher
And execute it with the command:
node memory.js
You should get the memory information printed in your console. Also, if you go to the Debug Console section of your app in the Pusher dashboard, you should see the events coming up:
Now let’s build the Android app.
Building the Android app
First, make sure to have the latest version of Android Studio. Then, create a new project:
We’re not going to use anything special, so we can safely support a low API level:
Next, create an initial empty activity:
And use the default name of MainActivity
with backward compatibility:
Once everything is set up, let’s install the project dependencies. First, add the following repository to your project level build.gradle
:
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
Next, in the dependencies
section of the build.gradle
file of your application module add:
dependencies {
...
compile 'com.android.support:recyclerview-v7:25.1.1'
compile 'com.github.PhilJay:MPAndroidChart:v3.0.2'
compile 'com.pusher:pusher-java-client:1.4.0'
compile 'com.google.code.gson:gson:2.4'
...
}
At the time of writing, the latest SDK version is 25, so that’s my target SDK version.
To graph the memory information we’re going to use MPAndroidChart, one of the most popular chart libraries for Android.
Sync the Gradle project so the modules can be installed and the project built.
Don’t forget to add the INTERNET
permission to the AndroidManifest.xml
file. This is required so we can connect to Pusher and get the events in realtime:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pusher.photofeed">
<uses-permission android:name="android.permission.INTERNET" />
<application>
...
</application>
</manifest>
Now, modify the layout file activity_main.xml
to set a line chart that fills all the available space:
<?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="com.pusher.memorygraph.MainActivity">
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
Open the com.pusher.memorygraph.MainActivity
class. Let’s start by defining some constants, like the info we’ll need to instantiate the Pusher object. Also, let’s define the total memory of our server as 16 (gigabytes) and set a maximum limit of 12 to draw a limit line in our chart.
public class MainActivity extends AppCompatActivity {
private LineChart mChart;
private Pusher pusher;
private static final String PUSHER_APP_KEY = "<INSERT_PUSHER_KEY>";
private static final String PUSHER_APP_CLUSTER = "<INSERT_PUSHER_CLUSTER>";
private static final String CHANNEL_NAME = "stats";
private static final String EVENT_NAME = "new_memory_stat";
private static final float TOTAL_MEMORY = 16.0f;
private static final float LIMIT_MAX_MEMORY = 12.0f;
...
}
In the next code block, you can see how the job of configuring the chart is divided into four functions, how Pusher is set up, specifying that when an event arrives, the JSON object will be converted to an instance of the class Stat
(that just contains the property memory
) and this will be added to the chart with the addEntry(stat)
method.
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mChart = (LineChart) findViewById(R.id.chart);
setupChart();
setupAxes();
setupData();
setLegend();
PusherOptions options = new PusherOptions();
options.setCluster(PUSHER_APP_CLUSTER);
pusher = new Pusher(PUSHER_APP_KEY);
Channel channel = pusher.subscribe(CHANNEL_NAME);
SubscriptionEventListener eventListener = new SubscriptionEventListener() {
@Override
public void onEvent(String channel, final String event, final String data) {
runOnUiThread(new Runnable() {
@Override
public void run() {
System.out.println("Received event with data: " + data);
Gson gson = new Gson();
Stat stat = gson.fromJson(data, Stat.class);
addEntry(stat);
}
});
}
};
channel.bind(EVENT_NAME, eventListener);
pusher.connect();
}
}
Let’s review all the methods defined above. First, setupChart()
configures some general options of the chart:
public class MainActivity extends AppCompatActivity {
...
private void setupChart() {
// disable description text
mChart.getDescription().setEnabled(false);
// enable touch gestures
mChart.setTouchEnabled(true);
// if disabled, scaling can be done on x- and y-axis separately
mChart.setPinchZoom(true);
// enable scaling
mChart.setScaleEnabled(true);
mChart.setDrawGridBackground(false);
// set an alternative background color
mChart.setBackgroundColor(Color.DKGRAY);
}
}
The setupAxes()
method configures the options of the X and Y axes and adds the limit line we talked about before:
public class MainActivity extends AppCompatActivity {
...
private void setupAxes() {
XAxis xl = mChart.getXAxis();
xl.setTextColor(Color.WHITE);
xl.setDrawGridLines(false);
xl.setAvoidFirstLastClipping(true);
xl.setEnabled(true);
YAxis leftAxis = mChart.getAxisLeft();
leftAxis.setTextColor(Color.WHITE);
leftAxis.setAxisMaximum(TOTAL_MEMORY);
leftAxis.setAxisMinimum(0f);
leftAxis.setDrawGridLines(true);
YAxis rightAxis = mChart.getAxisRight();
rightAxis.setEnabled(false);
// Add a limit line
LimitLine ll = new LimitLine(LIMIT_MAX_MEMORY, "Upper Limit");
ll.setLineWidth(2f);
ll.setLabelPosition(LimitLine.LimitLabelPosition.RIGHT_TOP);
ll.setTextSize(10f);
ll.setTextColor(Color.WHITE);
// reset all limit lines to avoid overlapping lines
leftAxis.removeAllLimitLines();
leftAxis.addLimitLine(ll);
// limit lines are drawn behind data (and not on top)
leftAxis.setDrawLimitLinesBehindData(true);
}
}
The setupData()
method just adds an empty LineData
object:
public class MainActivity extends AppCompatActivity {
...
private void setupData() {
LineData data = new LineData();
data.setValueTextColor(Color.WHITE);
// add empty data
mChart.setData(data);
}
}
The setLegend()
method sets the options of the legend for the data set that will be shown below the chart:
public class MainActivity extends AppCompatActivity {
...
private void setLegend() {
// get the legend (only possible after setting data)
Legend l = mChart.getLegend();
// modify the legend ...
l.setForm(Legend.LegendForm.CIRCLE);
l.setTextColor(Color.WHITE);
}
}
In turn, createSet()
will create the data set for the memory data configuring some options for its presentation:
public class MainActivity extends AppCompatActivity {
...
private LineDataSet createSet() {
LineDataSet set = new LineDataSet(null, "Memory Data");
set.setAxisDependency(YAxis.AxisDependency.LEFT);
set.setColors(ColorTemplate.VORDIPLOM_COLORS[0]);
set.setCircleColor(Color.WHITE);
set.setLineWidth(2f);
set.setCircleRadius(4f);
set.setValueTextColor(Color.WHITE);
set.setValueTextSize(10f);
// To show values of each point
set.setDrawValues(true);
return set;
}
}
The addEntry(stat)
method, the one used when an event arrives, will create a data set if none exists using the above method, add the entry from the Stat
instance that is passed as argument, notify the data has changed, and set the options to limit the view to 15 visible entries (to avoid the chart looking crowded):
public class MainActivity extends AppCompatActivity {
...
private void addEntry(Stat stat) {
LineData data = mChart.getData();
if (data != null) {
ILineDataSet set = data.getDataSetByIndex(0);
if (set == null) {
set = createSet();
data.addDataSet(set);
}
data.addEntry(new Entry(set.getEntryCount(), stat.getMemory()), 0);
// let the chart know it's data has changed
data.notifyDataChanged();
mChart.notifyDataSetChanged();
// limit the number of visible entries
mChart.setVisibleXRangeMaximum(15);
// move to the latest entry
mChart.moveViewToX(data.getEntryCount());
}
}
}
And finally, we override the method onDestroy()
to disconnect from Pusher when needed:
public class MainActivity extends AppCompatActivity {
...
@Override
public void onDestroy() {
super.onDestroy();
pusher.disconnect();
}
}
And we’re done, let’s test it.
Testing the app
Execute the app, either on a real device or a virtual one:
The following screen will show up:
Make sure the Node.js is running. When new data about the memory is received, it will show up in the graph:
Conclusion
Remember that you can find the final version of the Android app here and the Node.js process here.
Hopefully, this tutorial has shown you how simple it is to build a realtime graph in Android with Pusher and MPAndroidChart. You can improve the app by changing the design or type of graphic (a pie chart will work great to see the used vs the free memory), or show more information.
Remember that your free Pusher account includes 100 connections, unlimited channels, 200k daily messages, SSL protection, and there are more features than just Pub/Sub Messaging. Sign up here.
1 June 2017
by Esteban Herrera