Build a realtime table with Android
A basic understanding of Java and Node.js is needed to follow this tutorial.
If you are building an app that handles extensive amounts of data, you might want to implement realtime tables at some point. Let’s take a content management system for instance. Large amounts of data are added and removed often, and we would like the changes to be available to consumers in realtime.
In this tutorial, we will be utilizing the Pusher Channels Android SDK’s client-side library to quickly and easily build a realtime data table.
We’ll be using a few developer tools to achieve this fit, including:
- Android Studio - The Official IDE for Android Development, it provides the fastest tools for building apps on every type of Android device.
- Pusher Channels - A free, realtime, easy to use pub/sub service. Pusher makes realtime as easy as using basic events.
The flow of our app is that the user will fill out a form to add a new employee to an employees table and click a “save” button. This will send a POST request to our server. In this tutorial, we will use a simple NodeJS server to provide a single API endpoint.
Once our server receives the POST request, it will render the data to all connected clients, which will show the data on their tables in realtime.
Here is a glimpse of what we are going to build:
Set Up
Create a new project:
- Open Android Studio and select New Project from the File menu.
- Set the minimum SDK for the app to be API 16 (Android 4.1, Jelly Bean).
- Click through the wizard, ensuring that Empty Activity is selected. Leave the Activity Name set to
MainActivity
, and leave the Layout Name set toactivity_main
.
Add the Pusher Channels Android SDK to gradle:
Once you’ve set up your application on Android Studio, or your preferred IDE, then install Pusher as a dependency. In the build.gradle
file of your application module, add:
repositories {
maven { url 'http://clojars.org/repo' }
}
dependencies {
compile 'com.pusher:pusher-java-client:1.0.0'
compile 'com.loopj.android:android-async-http:1.4.9'
compile 'com.google.code.gson:gson:2.2.4'
}
Sync the gradle project. Pusher along with other modules we’ll need later, should now be installed.
However, you can find your Pusher credentials when you create an app after signing up for a free account. Be sure to keep them handy for when we start adding some realtime features.
Client-Side
Creating the form
In the created project, open the activity_main.xml
file and create the input form to collate the user details:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="10dp"
android:layout_centerVertical="true"
android:background="@drawable/layoutstyle"
android:layout_centerHorizontal="true">
<TextView
android:layout_gravity="center"
android:layout_margin="16dp"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#000"
android:fontFamily="serif"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Employee"/>
<EditText
android:background="@drawable/buttonstyle"
android:layout_margin="8dp"
android:id="@+id/edtName"
android:padding="8dp"
android:fontFamily="serif"
android:textColor="#fff"
android:hint="Name"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:padding="8dp"
android:background="@drawable/buttonstyle"
android:layout_margin="8dp"
android:id="@+id/edtAge"
android:fontFamily="serif"
android:textColor="#fff"
android:layout_width="match_parent"
android:hint="Age"
android:layout_height="wrap_content" />
<EditText
android:background="@drawable/buttonstyle"
android:layout_margin="8dp"
android:id="@+id/edtPosition"
android:padding="8dp"
android:textColor="#fff"
android:hint="Position"
android:fontFamily="serif"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:background="@drawable/buttonstyle"
android:layout_margin="8dp"
android:fontFamily="serif"
android:id="@+id/edtAddress"
android:padding="8dp"
android:textColor="#fff"
android:hint="Address"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/BtnSave"
android:layout_gravity="center"
android:fontFamily="serif"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="AddEmployee"
android:text="Save"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
Next we define some custom button styles and layout styles to give our app the desired user interface as can be seen in the snapshots earlier posted. So create two new drawable files called buttonstyle.xml and layoutstyle.xml and copy the codes therein into them.
These files only add round shapes to the EditText fields and LinearLayout. Hence, you could decide not to add my designs to your own work. However, to run the app with my designs and avert any xml errors, simply copy these files from the gist we attached above and paste in your own drawable files or simply delete every occurrence of buttonstyle.xml
or layoutstyle``.xml
in your xml. Your app will work just fine either way.
Then open the MainActivity.java
class and update it as follows to provide references to the EditText objects. This allows us to get the text from them:
package com.example.ekene.pushapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private EditText edtName, edtAge, edtPosition, edtAddress;
private Button btnSave;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edtName = (EditText)findViewById(R.id.edtName);
edtAddress = (EditText)findViewById(R.id.edtAddress);
edtAge = (EditText)findViewById(R.id.edtAge);
edtPosition = (EditText)findViewById(R.id.edtPosition);
}
}
Creating the table
Next we create the Employee’s Table where we’ll render the form data. We’ll do this with Android Studio’s TableLayout and TableRow. So inside the activity_main.xml
file, just below the form’s Linear Layout, we add:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000"
android:textSize="20sp"
android:fontFamily="serif"
android:text="Employees"
android:gravity="center"/>
<TableLayout
android:layout_marginTop="10dp"
android:id="@+id/table_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableRow
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/table_row1"
android:padding="10dp">
<TextView
android:id="@+id/name"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:textColor="#000"
android:text="Name"/>
<TextView
android:id="@+id/age"
android:textColor="#000"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Age"/>
<TextView
android:textColor="#000"
android:id="@+id/position"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Position"/>
<TextView
android:textColor="#000"
android:id="@+id/address"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="location"/>
</TableRow>
<View
android:layout_height="3dip"
android:layout_width="match_parent"
android:background="#ff0000"/>
</TableLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
Basically, this table is temporarily serving as a mock up for our application at this point. Later on we’’ll be creating the table dynamically with a ListView
.
Adding new employees to the table
To add new records to our table, we set up the event listener for the ‘Save’ button like so:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText edtName, edtAge, edtPosition, edtAddress;
private Button btnSave;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// get our input fields by its ID
edtName = (EditText)findViewById(R.id.edtName);
edtAddress = (EditText)findViewById(R.id.edtAddress);
edtAge = (EditText)findViewById(R.id.edtAge);
edtPosition = (EditText)findViewById(R.id.edtPosition);
// get our button by its ID
btnSave = (Button) findViewById(R.id.BtnSave);
// set its click listener
btnSave.setOnClickListener(this);
}
We’ll then define a method that will execute when the save button is clicked to get the data from our input fields and post to the server.
@Override
public void onClick(View v) {
addEmployee();
}
private void addEmployee() {
}
The addEmployee()
method will simply get the values from the EditText
objects and POST it to the server. In this tutorial, we’ll be using the AsyncHTTPClient library to send records to our server.
So let’s go ahead with creating and sending our request parameters in the MainActivity.java
file:
package com.example.ekene.pushapp;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.loopj.android.http.RequestParams;
import org.json.JSONArray;
import android.text.TextUtils;
public class MainActivity extends AppCompatActivity {
private void addEmployee(View v) {
String employeeName = edtName.getText().toString();
String employeeAge = edtAge.getText().toString();
String employeePosition = edtPosition.getText().toString();
String employeeAddress = edtAddress.getText().toString();
// return if the input fields are blank
if (TextUtils.isEmpty(employeeName) && TextUtils.isEmpty(employeeAge) &&
TextUtils.isEmpty(employeePosition)&&
TextUtils.isEmpty(employeeAddress)) {
return;
}
RequestParams params = new RequestParams();
// set our JSON object
params.put("name", employeeName);
params.put("age", employeeAge);
params.put("position", employeePosition);
params.put("address", employeeAddress);
// create our HTTP client
AsyncHttpClient client = new AsyncHttpClient();
...
}
We’ll then define a String variable called RECORDS_ENDPOINT
in MainActivity
and set it to point to the URL of our server e.g:
private static final String RECORDS_ENDPOINT = "http://localhost:3000/records";
Then let’s make it so that when the request is successful it clears the inputFields
, or when it fails it alerts the user that it “Couldn’t Post” with a Toast.
So we continue creating our HTTP client inside the MainActivity
like so:
....
client.post(RECORDS_ENDPOINT, params, new JsonHttpResponseHandler(){
@Override
public void onSuccess(
int statusCode,
cz.msebera.android.httpclient.Header[] headers,
JSONArray response) {
super.onSuccess(statusCode, headers, response);
runOnUiThread(new Runnable() {
@Override
public void run() {
edtName.setText("");
edtAge.setText("");
edtPosition.setText("");
edtAddress.setText("");
}
});
}
@Override
public void onFailure(
int statusCode,
cz.msebera.android.httpclient.Header[] headers,
String responseString,
Throwable throwable) {
super.onFailure(statusCode, headers, responseString, throwable);
Toast.makeText(
getApplicationContext(), "Couldn't Post!",
Toast.LENGTH_LONG
).show();
}
});
}
}
At this point the client is set up to send the input to the server. Next we’ll set up our mini API
Server-side
At this point we will integrate Pusher at the back end. Pusher is a simple hosted API for quickly, easily and securely implementing realtime two-way functionality on web and mobile apps. To achieve this, we’ll need to set it up on the server-side.
Install NodeJS and Express if you haven’t already. Generate your table-backend with:
$ express table-backend
$ cd table-backend
$ npm install
Now install the Pusher Node library with:
$ npm install pusher --save
Now we initialize the Pusher
object in our app.js
file with the application credentials:
var Pusher = require('pusher');
var express = require('express');
var options = PusherOptions();
options.setCluster(PUSHER_APP_CLUSTER);
var pusher = new Pusher({
appId: "your app id",
key: "your app key",
secret: "your app secret"
});
Next we create the endpoint that receives JSON from the client. Then we’ll fire up a Pusher event called new_record
on a channel called records
, passing along the data we received from the client.
app.post('/records', function(req, res){
var record = req.body;
pusher.trigger('records', 'new_record', record);
res.json({success: 200});
});
Next open your AndroidManifest.xml
file and enable internet permissions. Just before the Application tag, add:
<uses-permission android:name="android.permission.INTERNET"/>
Now let’s fire up our server, and run the mobile app either on an emulator or on any android device. Meanwhile, open up the Pusher Debug Console for your app on your dashboard. Then on your emulator or device, you should see the Add Employee form, fill in the fields and and click the Save
button, you should see the information you just supplied pop up in realtime on your Debug Console.
Render Live Table Records
We’ve come a long way but we are not done, Let’s now have our Android client listen for incoming employee records, and show them in a list.
So in activity_main.xml
, add a ListView
as the last child of LinearLayout
and above the TableLayout
that wraps our table headings. Your layout XML starting from the TableLayout should look like so:
<TableLayout
android:layout_marginTop="10dp"
android:id="@+id/table_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableRow
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/table_row1"
android:padding="10dp">
<TextView
android:id="@+id/name"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:textColor="#000"
android:text="Name"/>
<TextView
android:id="@+id/age"
android:textColor="#000"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Age"/>
<TextView
android:textColor="#000"
android:id="@+id/position"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Position"/>
<TextView
android:textColor="#000"
android:id="@+id/address"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="location"/>
</TableRow>
<View
android:layout_height="3dip"
android:layout_width="match_parent"
android:background="#ff0000"/>
</TableLayout>
// add a listview to display our table records
<ListView
android:id="@+id/records_view"
android:layout_width="match_parent"
android:layout_height="500dp"
android:layout_marginTop="16dp">
</ListView>
</LinearLayout>
</ScrollView>
</LinearLayout>
In order to display each message within the ListView
, we’ll have to create an adapter that turns a list into a set of views. In our MainActivity onCreate
method, let’s bind our ListView
to this adapter like so: NB we haven’t created the adapter yet, we’ll do that in a bit.
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private RecordAdapter recordAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
recordAdapter= new RecordAdapter(this, new ArrayList<Record>());
final ListView recordsView = (ListView) findViewById(R.id.records_view);
recordsView.setAdapter(recordAdapter);
}
Next we create the Record.java
class which comprises a single row in the List:
public class Record {
public String age;
public String name;
public String position;
public String address;
}
Next we create the adapter. Create a new class RecordAdapter
. We initialized it in the MainActivity
Class with our MainActivity
‘s context. We’ll create it like so:
public class RecordAdapter extends BaseAdapter {
private Context recordContext;
private List<Record> recordList;
public RecordsAdapter(Context context, List<Record> records) {
recordList = records;
recordContext = context;
}
Because we extended BaseAdapter
, Android Studio will prompt us to implement it’s three associating methods getCount
, getItem
and getItemId
, which we can do like so:
@Override
public int getCount() {
return recordList.size();
}
@Override
public Object getItem(int i) {
return recordList.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
Finally we’ll have to implement a method called getView()
that will convert an item in the ArrayList
of Records
to a view. But first we need to create a RecordViewHolder
private class to encapsulate the views we would like to be part of the message. In this case, we’re going to have a:
- nameView - for the employees name
- ageView - for the employees name
- nameView - for the employees name
- nameView - for the employees name
So within our RecordAdapter
, add a private nested class:
private static class RecordViewHolder {
public TextView nameView;
public TextView positionView;
public TextView ageView;
public TextView addressView;
}
Next we create another layout resource file called record.xml
. This layout will hold the views we defined in the RecordViewHolder
class and render it to the list. Hence it’ll contain four TextViews
for the name, age, position and address respectively.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/record_name"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:textColor="#000"
android:text="Name"/>
<TextView
android:id="@+id/record_age"
android:textColor="#000"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Age"/>
<TextView
android:textColor="#000"
android:id="@+id/record_position"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="Position"/>
<TextView
android:textColor="#000"
android:id="@+id/record_address"
android:fontFamily="serif"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="1"
android:text="location"/>
</LinearLayout>
Now that we have a RecordViewHolder
to encapsulate the visual elements that comprise a record, and a record.xml
layout to inflate those elements into, we can go ahead and implement our getView
method inside our RecordAdapter class like so:
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
RecordViewHolder holder;
if (view ==null){
LayoutInflater recordInflater = (LayoutInflater)
recordContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
view = recordInflater.inflate(R.layout.record, null);
holder = new RecordViewHolder();
holder.ageView = (TextView) view.findViewById(R.id.record_age);
holder.nameView = (TextView) view.findViewById(R.id.record_name);
holder.positionView = (TextView) view.findViewById(R.id.record_position);
holder.addressView = (TextView) view.findViewById(R.id.record_address);
view.setTag(holder);
}else {
holder = (RecordViewHolder) view.getTag();
}
Record record = (Record) getItem(i);
holder.nameView.setText(record.name);
holder.ageView.setText(record.age);
holder.positionView.setText(record.position);
holder.addressView.setText(record.address);
return view;
}
What we want to do now is, when we receive an event from Pusher about a new record, we want to add that new record to our RecordAdapter
and update our recordsList
with that new employee record. Here’s how we achieve that:
//...
public class RecordsAdapter extends BaseAdapter {
//...
public void add(Record record) {
recordList.add(record);
notifyDataSetChanged();
}
}
This will add the record to the recordsList
and notifyDataSetChange()
will refresh the adapter, showing the new record.
Now we can go back to MainActivity
and start listening for Pusher records:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// initialize Pusher
Pusher pusher = new Pusher("pusher_key");
pusher.setCluster("app_cluster");
// subscribe to our "records" channel
Channel channel = pusher.subscribe("records");
// listen for the "new_record" event
channel.bind("new_record", new SubscriptionEventListener() {
...
});
// connect to the Pusher API
pusher.connect();
}
Now that we have initialized Pusher, connected to the API, and subscribed to the records
channel, we can add our SubscriptionEventListener
to execute when an event comes in. All we’ll need to do is parse the JSON (for this example we used the Gson library to parse it into the Record
object) and then add it to the RecordAdapter
inside the MainActivity
like so:
channel.bind("new_record", new SubscriptionEventListener() {
@Override
public void onEvent(String channelName, String eventName, final String data) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Gson gson = new Gson();
Record record = gson.fromJson(data, Record.class);
recordAdapter.add(record);
recordsView.setSelection(recordsAdapter.getCount() - 1);
}
});
}
});
At this point, whenever we have a new_record
event come in, we simply add it to our RecordAdapter
, and the new record will appear in realtime! wow, that was amazing!
Now run your app on an emulator or any android device, and give it a spin. If you encounter any issues, do consult the source code.
If you have an android device, you can install the app here and see how it works.
Conclusion
With this walk through, you should be able to build cross platform realtime apps with ease. Other Pusher features are available but their use depends on the requirements of your app. You can have a look at Pusher’s documentation for a deeper comprehension of the project.
Appendix: Pusher Setup
-
Sign up for a free Pusher account:
-
Create a new app by selecting Apps on the sidebar and clicking Create New button on the bottom of the sidebar:
-
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with for a better setup experience:
-
You can retrieve your keys from the App Keys tab:
19 January 2018
by Christian Nwamba