Build

Build Android Messaging App: Scaling and Channel Groups

Michael Carroll on Jul 29, 2018
Build Android Messaging App: Scaling and Channel Groups

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:

  • Display a dynamic list of the latest message received from a set of channels.
  • Use more advanced features for Wildcard Subscriptions or Channel Groups.
  • Use an extensible data format that can be changed to send/receive any type of structured data.
  • Adapt to compute other statistics on channel data (show highest or lowest instead of most recent).

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:

Screen Capture of Multiplexing Feature

What is Multiplexing and Channel Groups?

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.

Understanding the Stream Controller

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: this is subscribing to multiple channels explicitly at once (up to about 100 channels effectively).
  • Wildcard Subscribe: this allows subscribing to multiple channels that use dot-separated channel names to define a hierarchy, for example foo.bar.baz, and subscribing to foo.* or foo.bar.* accordingly.
  • Channel Groups: this involves creating a channel group within PubNub that corresponds to up to 2,000 channels.

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.

Subscribing to a Channel

We want to subscribe to a channel so we can receive presence events 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(s) subscription.

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:

  • The first step is to register the callbacks with the PubNub object – you only need to do this once for each callback instance.
  • The next step is to subscribe to the desired channels.
  • You only need to subscribe once (unless you subsequently call unsubscribe() on the channel(s) and want to resubscribe).

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.

Dynamic Data and the Adapter Class

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 using notifyDataSetChanged().
  • 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 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:

  • We should only show one message for each channel.
  • We should only show the latest message received by the client for each channel.
  • We should move the latest updated channel to the top.

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.

Advanced Stream Controller Features

While multiplexing channels is the most common use for the Stream Controller API, it has several other useful features.

Wildcard Subscribe

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).

Add Channel(s) to a Group

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
 }
 }
 );

Remove Channel(s) from a Group

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
 }
 }
 );

Delete Channel Group

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:

  • A “last message received” tab with dynamic list of messages received from which users across multiple PubNub channels
  • Reviewed API methods for Wildcard Subscription and Channel Groups
  • Use an extensible data format that can be changed to send/receive any type of structured data

Wrapping Up

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.