This is the final part of our four-part series on building an Android messaging app with PubNub. In our previous part, we built our real-time user list.
In this tutorial, we dive into Multiplexing and other techniques for managing multiple channel subscriptions in PubNub, part of the broader feature group called Stream Controller.
This will allow you to create a “latest message from channels” tab that can:
To do this, we dive into the core concepts of subscriptions and message callbacks, more advanced Stream Controller use cases, as well as the Android machinery to make it all possible.
For the purposes of this article, we’ll be looking at the Multiplex feature – more specifically, the “latest message view” tab. That’s this part:
So what are Multiplexing and Channel Groups? What fun would an app be if it only had one channel? PubNub gives developers the ability to create an unlimited number of channels. Using one connection for each channel subscription would quickly become unwieldy. Similarly, it’s often useful to give a group of channels a “nickname” to allow for easier subscription (for example, creating an alias for all the component stocks of the Dow Jones or S&P 500 indexes).
PubNub provides Multiplexing to allow one device connection to support dozens of channel subscriptions. For cases where it’s necessary to subscribe to hundreds or thousands of channels, PubNub provides a feature called Channel Groups to create a compound subscription. Together, this set of features comprises the Stream Controller add-on for PubNub.
Multiplexing has a few nuances we should talk about before diving in further. The first is that Stream Controller is an add-on, so you’ll need to make sure it’s enabled in your application configuration.
There are 3 types of subscription techniques with Stream Controller:
Multiplexing is the simplest, most non-intrusive way to subscribe to multiple channels. The client will optimize the subscription to use only a single connection to the server to conserve resources.
Wildcard Subscribe is the next easiest option for subscribing to multiple channels, although it is slightly heavier weight because it imposes design constraints on your channel names and is not as useful if channel subscriptions are not hierarchical.
Channel Groups is the most sophisticated feature in Stream Controller, although it requires the most effort to put into place. Depending on your application needs, certain clients will set up the channel groups, and/or other clients will subscribe to them. Using this technique, clients may subscribe to up to 10 channel groups at once with up to 2,000 channels each, allowing them to subscribe up to a total of 20,000 channels.
We want to subscribe to a channel so we can receive presence events and display them in the UI ListView. That takes three steps:
Once the app completes the subscription, the callback will be invoked for each new message event that comes from PubNub across all the channels.
Here’s how to create a subscription callback:
public class MultiPnCallback extends SubscribeCallback { private static final String TAG = MultiPnCallback.class.getName(); private final MultiListAdapter multiListAdapter; public MultiPnCallback(MultiListAdapter multiListAdapter) { this.multiListAdapter = multiListAdapter; } @Override public void status(PubNub pubnub, PNStatus status) { // no status handling for simplicity } @Override public void message(PubNub pubnub, PNMessageResult message) { try { Log.v(TAG, "multi(" + JsonUtil.asJson(message) + ")"); JsonNode jsonMsg = message.getMessage(); LinkedHashMap<String, Object> initial = new LinkedHashMap<String, Object>(); initial.put("channel", message.getSubscribedChannel()); initial.putAll(JsonUtil.convert(jsonMsg, LinkedHashMap.class)); MultiPojo mlMsg = JsonUtil.convert(initial, MultiPojo.class); this.multiListAdapter.add(mlMsg); } 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 used because we want this callback to handle message events across the subscribed channels. Note that we use message.getSubscribedChannel() to obtain the channel name from the incoming PubNub event.
In this case, the presence() method is not used for any of the multiplexed channels, since we are only interested in message events.
Here’s a recap of how to do the channel subscription: make sure to register the Muliplex callback as a listener on the PubNub object.
private final void initChannels() { ... this.mPubnub_Multi.addListener(mMultiPnCallback); this.mPubnub_Multi.subscribe().channels(MULTI_CHANNELS).execute(); ...
Let’s review that code a bit:
That’s it! Now, as channel message events come in from the Multiplexed channels, 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 MultiListAdapter in the sample application works.
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:
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 MultiListAdapter extends ArrayAdapter { private final Context context; private final LayoutInflater inflater; private final List multiList = new ArrayList(); private final Map<String, MultiPojo> latestMultiMessage = new LinkedHashMap<String, MultiPojo>(); public MultiListAdapter(Context context) { super(context, R.layout.list_row_multi); this.context = context; this.inflater = LayoutInflater.from(context); } @Override public void add(MultiPojo message) { if (latestMultiMessage.containsKey(message.getChannel())) { this.multiList.remove(message.getChannel()); } this.multiList.add(0, message.getChannel()); latestMultiMessage.put(message.getChannel(), message); ((Activity) this.context).runOnUiThread(new Runnable() { @Override public void run() { notifyDataSetChanged(); } }); } @Override public View getView(final int position, View convertView, ViewGroup parent) { String channel = this.multiList.get(position); MultiPojo multiMsg = this.latestMultiMessage.get(channel); MultiListRowUi msgView; if (convertView == null) { msgView = new MultiListRowUi(); convertView = inflater.inflate(R.layout.list_row_multi, parent, false); msgView.channel = (TextView) convertView.findViewById(R.id.channel); 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 = (MultiListRowUi) convertView.getTag(); } msgView.channel.setText(multiMsg.getChannel()); msgView.sender.setText(multiMsg.getSender()); msgView.message.setText(multiMsg.getMessage()); msgView.timestamp.setText(multiMsg.getTimestamp()); return convertView; } ... }
There are a couple things we should mention as features & enhancements for the Muliplexing “last message received” list:
This is similar to what you might use for a stock market or other pricing app, or a dynamic mapping or graphing application.
To implement this, we keep a List in the Adapter to represent the channel names in order, and keep a LinkedHashMap<String, MultiPojo> to make it easy to update the data value corresponding to a given channel.
One last thing to note on this topic is that updates to the UI need to happen on the UI thread; that’s 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/or crash the app.
While multiplexing channels is the most common use for the Stream Controller API, it has several other useful features.
Using wildcard subscribe is not much different from using a normal subscribe call. The main requirement is to use a dot-separated notation for your channel names, where each channel name has up to three dot-separated segments. In the code below, if there are three channels, a.a, a.b, and a.x, this subscribe code will listen to them all:
private final void initChannels() { ... this.mPubnub_Multi.addListener(mMultiPnCallback); this.mPubnub_Multi.subscribe().channels("a.*").execute(); ...
Pretty awesome, if your application design allows this pattern! (But it can be difficult to retro-fit onto existing apps that don’t use this pattern).
Channel Groups are a powerful feature for giving a single alias to a group of up to 2,000 channels. The code for creating a channel group is pretty straightforward – just start adding one or more channels to the channel group name.
this.mPubnub_Multi.addChannelsToChannelGroup() .channelGroup("the_channel_group_name") .channels(Arrays.asList("channel_1", "channel_2")).async( new PNCallback() { @Override public void onResponse(PNChannelGroupsAddChannelResult result, PNStatus status) { // reject if status.isError(), process otherwise } } );
Removing channels from a channel group is just as straightforward as adding them:
this.mPubnub_Multi.removeChannelsFromChannelGroup() .channelGroup("the_channel_group_name") .channels(Arrays.asList("channel_1", "channel_2")).async( new PNCallback() { @Override public void onResponse(PNChannelGroupsRemoveChannelResult result, PNStatus status) { // reject if status.isError(), process otherwise } } );
When a channel group is no longer necessary, you can delete it as follows:
this.mPubnub_Multi.deleteChannelGroup().channelGroup("the_channel_group_name").async( new PNCallback() { @Override public void onResponse(PNChannelGroupsDeleteGroupResult result, PNStatus status) { // reject if status.isError(), process otherwise } } );
Let’s take a second to recap what we built in this part:
That’s it! We now have a fully-functioning Android messaging application powered by PubNub! We hope you enjoyed our four part series, and please feel free to contact us if you have any questions or comments.
There are common underlying technologies for a dating app, and in this post, we’ll talk about the major technologies and designs...
Michael Carroll
How to use geohashing, JavaScript, Google Maps API, and BART API to build a real-time public transit schedule app.
Michael Carroll
How to track and stream real-time vehicle location on a live-updating map using EON, JavaScript, and the Mapbox API.
Michael Carroll