Android Chat Tutorial with Java: All Things Messaging (2/4)
This is Part Two of our four-part series on building a mobile chat app for Android with PubNub. In our previous post, we gave an overview of the technologies we’ll use and showed you how to set up your Android environment.
Now it’s time to start building chat. We’ll cover:
- Publishing a message from a user/device to a PubNub channel.
- Keeping an ordered list of received messages from a PubNub channel.
- Using a structured message format (JSON map) to provide extensibility of data being sent/received.
By the end of this part, you’ll have a better understanding of channel subscribe()
& message callbacks, the publish()
method, as well as the Android machinery to make it all possible.
Implementing a Chat Tab
Before we implement the chat tab feature, let’s take a moment to understand what lies ahead.
- Understanding the code patterns.
- Initializing PubNub.
- Subscribing to a channel.
- Publishing to a channel.
We use patterns in the code and layouts to better isolate each feature, so it’s worthwhile to review the lingo first. Once you understand it, initializing PubNub is pretty straightforward. Similarly, Subscribing and Publishing to channels is very easy! The hardest part is figuring out how to bridge between the real-time connection and your app’s logic.
Understanding Code Patterns
Looking at the Android project, you’ll see a lot of code. If you believe that the code is the documentation, I should have included SPOILER ALERT above! If you appreciate documentation, here’s a brief description of how it’s all laid out.
- LoginActivity and MainActivity: the main views and entry points of the Android application.
- Fragments: one for each feature Tab, as described below under
TabContentFragment.
- Layouts: for Activities, Fragments (Tabs), List Rows, in the handy
res/layout
folder. - Menu definition: in the handy
res/menu
folder. - Feature-specific Java code: included in the
pubsub
,presence
, andmulti
directories.ListAdapter.java
: bridges the dynamic data collection (in this case, the list of chat messages) from pure Java to the Android UI.ListRowUi.java
: a bare-bones holder object with one field per UI element in the row (makes it easy to write the adapter above).*PnCallback.java
: a message callback implementation that handles all of the events PubNub sends to your app: status, message, and presence events.*Pojo.java
: a “plain old Java object” that represents the pure data of a message as it travels between the app and PubNub (or vice versa).*TabContentFragment.java
: an Android fragment implementation for each Tab of the application (used by theMainActivityTabManager
).
Initializing PubNub
In order to use PubNub, you need to make friends with the PubNub
class. You can create several instances of App Context if you need multiple connections with different keys, callbacks, or other configurations. In our sample app, we create two App Context because we want to separate the Pub/Sub and Presence listeners from the Multiplexing feature. That’s a rare case – you’ll probably only need one instance.
We initialize the PubNub
object(s) in the MainActivity
class right after the user logs in via the LoginActivity
. This is because the username is required as the UUID field of the configuration object below. Note: if your application allows rapid switching between users, you may want to think carefully about how to create and destroy the PubNub
objects accordingly.
PNConfiguration config = new PNConfiguration(); config.setPublishKey(Constants.PUBNUB_PUBLISH_KEY); config.setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY); config.setUuid(this.mUsername); config.setSecure(true); this.mPubnub_DataStream = new PubNub(config);
Just to recap the code above.
- Step 1: create a
PNConfiguration
object - Step 2: pass in the Publish and Subscribe keys
- Step 3: pass in the user/device UUID (whatever you like, as long as it’s unique – this UUID will also used by the presence API)
- Step 4: set the secure option to use TLS
- Step 5: create a new
PubNub
instance using the specifiedPNConfiguration
One thing we should also note – if you really want to dive into the fluent API, you can also do something like this:
this.mPubnub_DataStream = new PubNub(new PNConfiguration() .setPublishKey(Constants.PUBNUB_PUBLISH_KEY) .setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY) .setUuid(this.mUsername).setSecure(true));
Now we’re having some fun with it!
Subscribing to a Channel
Now that we’ve established our PubNub connection, let’s subscribe to a channel so we can receive messages and display them in the UI ListView. That takes three steps:
- Step 1 (optional): Create an Adapter to bridge between the callback and the application UI View.
- Step 2: Create a subscription callback (that calls methods in the Adapter, if applicable).
- Step 3: Perform the channel subscription.
Once the app completes the subscription, a callback is invoked for each new channel event that comes from PubNub.
Here’s how to create a subscription callback:
public class PubSubPnCallback extends SubscribeCallback { @Override public void status(PubNub pubnub, PNStatus status) { // for common cases to handle, see: https://www.pubnub.com/docs/java/pubnub-java-sdk-v4 } @Override public void message(PubNub pubnub, PNMessageResult message) { try { Log.v(TAG, "message(" + JsonUtil.asJson(message) + ")"); JsonNode jsonMsg = message.getMessage(); PubSubPojo dsMsg = JsonUtil.convert(jsonMsg, PubSubPojo.class); this.pubSubListAdapter.add(dsMsg); } catch (Exception e) { e.printStackTrace(); } } @Override public void presence(PubNub pubnub, PNPresenceEventResult presence) { // no presence handling for simplicity } }
In the code above, the status()
method is where we handle events such as connection errors and reconnect events, publish, or subscribe errors.
The message()
method is where the real magic happens. We take JSON from the incoming message and convert it into one of our specialized POJO value objects, before passing it on to the adapter.
We won’t spoil the surprise, but the presence()
method is where we’d put logic for handling presence events if we wanted to handle it all in a single callback. In the case of this application, we create multiple callbacks for code separation.
Here’s how to do the channel subscription:
private final void initChannels() { this.mPubnub_DataStream.addListener(this.mPubSubPnCallback); this.mPubnub_DataStream.addListener(this.mPresencePnCallback); ... this.mPubnub_DataStream.subscribe().channels(PUBSUB_CHANNEL).withPresence().execute(); ...
Let’s review the code a bit:
- The first step registers the callbacks with the PubNub object – you only need to do this once for each callback instance.
- The next step subscribes to the desired channel, optionally specifying
withPresence()
if you like. - You only need to subscribe once (unless you subsequently call
unsubscribe()
on the channel and want to resubscribe).
That’s it! Now, as messages come in on the Pub/Sub channel, the corresponding callback(s) will be invoked. Depending on your application, this might be all you need! Chances are though, you’ll need to propagate the data into your UI – that’s what we alluded to when we talked about the Adapter above. Let’s assume we’re using something like a ListView
to display data, so we’ll want to check out how the PubSubListAdapter
in the application works.
What the Adapter Does?
When would we need an adapter? Like we said before, the Adapter class is a way to bridge between our dynamic data collection and the Android UI. Here are the main aspects of the Adapter:
- Part 1 (optional): a concrete Java collection containing the “real” values.
- Part 2: mutation methods (in our case, just
add()
) to propagate new values from the PubNub callback usingnotifyDataSetChanged().
- Part 3: a
getView()
method to instantiate each row of the View.
The reason why Part 1 is optional is that in some cases, the Java collection might be extremely large. It may not be possible, nor desirable, to keep all of those values in memory. Along those lines, it shouldn’t be hard to imagine a scenario where we omit the values
List below, and use dynamic requests to a SQLite DB, file, or other data store in the getView()
call accordingly.
public class PubSubListAdapter extends ArrayAdapter { private final Context context; private final LayoutInflater inflater; private final List values = new ArrayList(); public PubSubListAdapter(Context context) { super(context, R.layout.list_row_pubsub); this.context = context; this.inflater = LayoutInflater.from(context); } @Override public void add(PubSubPojo message) { this.values.add(0, message); ((Activity) this.context).runOnUiThread(new Runnable() { @Override public void run() { notifyDataSetChanged(); } }); } @Override public View getView(final int position, View convertView, ViewGroup parent) { PubSubPojo dsMsg = this.values.get(position); PubSubListRowUi msgView; if (convertView == null) { msgView = new PubSubListRowUi(); convertView = inflater.inflate(R.layout.list_row_pubsub, parent, false); msgView.sender = (TextView) convertView.findViewById(R.id.sender); msgView.message = (TextView) convertView.findViewById(R.id.message); msgView.timestamp = (TextView) convertView.findViewById(R.id.timestamp); convertView.setTag(msgView); } else { msgView = (PubSubListRowUi) convertView.getTag(); } msgView.sender.setText(dsMsg.getSender()); msgView.message.setText(dsMsg.getMessage()); msgView.timestamp.setText(dsMsg.getTimestamp()); return convertView; } ... }
We should mention a few things about the add()
method above. First, we’re prepending new elements to the values list using List.add(index, value)
, where index
is 0. In other cases, you may just want to use addition at the end of the list using List.add(value)
.
Secondly, we’re using a RowUi object to hold all the UI elements that we need to update for a given row. That is what the getTag()
and setTag()
calls on the convertView
instance are all about.
Before we forget, here’s the RowUi object for the Pub/Sub feature – it’s very small.
public class PubSubListRowUi { public TextView sender; public TextView message; public TextView timestamp; }
One other thing to note is that updates to the UI need to happen on the UI thread. This is why we keep a reference to the context in the Adapter, and make sure to run the notifyDataSetChanged()
call on the UI thread. If you don’t do that, you’ll see a ton of warnings in the logs and probably crash the app.
Publishing to a Channel
Publishing to a Channel is just about the easiest task you can accomplish with the PubNub API. It’s not even strictly required to be subscribed to a channel before publishing. The only thing that’s a little tricky is what to do with the response inside the callback. You have two cases to consider, the success and error cases. In the case of our sample application, we obtain the new message text from an EditText field with the id new_message
.
Based on that data, we create a new message object (think JSON) that includes:
- The sender username.
- The message itself.
- The timestamp in ISO format.
For the visual learners out there, this is the object we’re talking about:
{ "sender":"fred", "message":"hello world!", "timestamp":"20160606T130201.123Z" }
Note: these fields are just what we’ve chosen to put in our message for the sample application. You might choose totally different attributes for your application (name, age, favorite food…). Even though the attributes are changed, the publish/subscribe mechanism is the same. That’s the beauty of the PubNub publish/subscribe API!
Once we have our data object to send, it’s very easy to call the PubNub.publish()
API with the specified destination channel. You might take a moment to pause and take in the beauty of the new fluent API. The only other thing we should mention is that you’ll need to pass a PNCallback along which will be invoked when the operation succeeds or fails.
public void publish(View view) { final EditText mMessage = (EditText) MainActivity.this.findViewById(R.id.new_message); final Map<String, String> message = ImmutableMap.<String, String>of( "sender", MainActivity.this.mUsername, "message", mMessage.getText().toString(), "timestamp", DateTimeUtil.getTimeStampUtc()); MainActivity.this.mPubnub_DataStream.publish().channel(Constants.CHANNEL_NAME).message(message).async( new PNCallback() { @Override public void onResponse(PNPublishResult result, PNStatus status) { try { if (!status.isError()) { mMessage.setText(""); Log.v(TAG, "publish(" + JsonUtil.asJson(result) + ")"); } else { Log.v(TAG, "publishErr(" + JsonUtil.asJson(status) + ")"); } } catch (Exception e) { e.printStackTrace(); } } } ); }
Saavy users may be asking, “Hey, why are we using async()
here instead of sync()
?” That’s a great question. In our case, since we’re invoking the publish()
method as a button onClick()
event, we need to avoid the Android error case for “network operations called from a UI event handler.” If the publish is happening inside a non-UI thread, we could just use the sync()
method to publish the message and receive the publish result synchronously instead.
Other Classes
There are 2 other Java classes for the presence feature that we should show for completeness: the Pojo
class and the TabContentFragment
class.
The Pojo
class represents the pure data of the value we’re consuming in the application. As a good practice, we recommend implementing equals()
, hashCode()
and toString()
for this type of object so it behaves well with Java Collection classes.
public class PubSubPojo { private final String sender; private final String message; private final String timestamp; public PubSubPojo(@JsonProperty("sender") String sender, @JsonProperty("message") String message, @JsonProperty("timestamp") String timestamp) { this.sender = sender; this.message = message; this.timestamp = timestamp; } public String getSender() { return sender; } public String getMessage() { return message; } public String getTimestamp() { return timestamp; } @Override public boolean equals(Object obj) { ... } @Override public int hashCode() { ... } @Override public String toString() { ... } }
The only other thing worth mentioning about this class is that we use Jackson @JsonProperty
annotations in the constructor as a hint to the Jackson JSON library to make it very easy to serialize and deserialize the object on the wire. Haven’t heard about Jackson? Check it out, it’s awesome!
The other class we’ll talk about is the TabContentFragment
class. It’s a minimal class designed for handling View lifecycle events, onCreateView()
(and possibly onDestroyView()
in some cases). This class is essentially a bridge between the TabManager
class and the Layout to be instantiated.
public class PubSubTabContentFragment extends Fragment { private PubSubListAdapter psAdapter; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_pubsub, container, false); ListView listView = (ListView) view.findViewById(R.id.message_list); listView.setAdapter(psAdapter); return view; } public void setAdapter(PubSubListAdapter psAdapter) { this.psAdapter = psAdapter; } }
In our case, we also add a setter method for the Adapter
class so that the ListView
inside the Tab Fragment can receive it upon instantiation. That’s what allows the view inside the Tab to receive dynamic updates (through the Adapter
and originally coming from the PnCallback
class).
That’s pretty much about it! There’s just one small topic worth thinking about before we go – clean up!
Cleaning Up
So, this has all been great, but there are a bunch of messy cases to think about when building a real-world app.
- What if users are on a flaky network connection?
- How do we log in and out of the application?
- What if we want to go offline?
- How do we handle Android app lifecycle events?
These are all very intricate questions, some of which we’ll have to tackle in a future blog entry. In the meantime, we’ll highlight this code for unsubscribing from channels, removing listeners and destroying a PubNub object. If you choose to do this, keep in mind there is a more elegant solution to network events involving PubNub.reconnect()
– it might be worth looking into that to see if it can cover your use case.
private void disconnectAndCleanup() { ... if (this.mPubnub_DataStream != null) { this.mPubnub_DataStream.unsubscribe().channels(PUBSUB_CHANNEL).execute(); this.mPubnub_DataStream.removeListener(this.mPubSubPnCallback); this.mPubnub_DataStream.removeListener(this.mPresencePnCallback); this.mPubnub_DataStream.stop(); this.mPubnub_DataStream = null; } ...
Hope this helps! The key points are to unsubscribe from channels (so that Presence leave events are generated on other clients), remove all listeners (so that you don’t have multiple callback registrations), call stop()
(to tear down the network connection), and set the field to null (so you can be sure to avoid any errors caused by using a stopped connection).
Running the code
Running the code should be as easy as clicking the play button in the Android Studio toolbar. You can use a connected device or an AVD in the emulator to run the application. As you sign into the application from multiple devices, you can exchange chat messages in the Pub/Sub tab, the Presence tab will show an updated member list, and the Multi tab will show the latest message sent by each device to a random channel. Here’s what you’ll find:
Next Steps
Hopefully, you’ve seen what’s possible with Publish/Subscribe messaging! Let’s take a second to recap what we built:
- A Chat tab with dynamic list of messages inbound from PubNub.
- The ability for a user to publish messages to a channel.
- An extensible data format that allows us to send & receive any kind of structured data on the channel.
Next is Part 3, where we’ll add a real-time user list, allowing you to see which users are online and offline in real time, and initiate a chat from it.