Some applications need real-time messaging built in to implement various use cases. Some require publish, subscribe or both. In addition, some other apps may rely on presence, covering a wide range of use cases such as connections and disconnections from other users or systems.
My current company, Ingenio, enables connections between advisors and users, creating a marketplace where people can get the best life advices and interact via voice and real time chat.
In the current Flutter development of the new advisor application, we had the requirement for delivering real time messages using the platform current in used by our web and native consumer mobile apps: PubNub.
PubNub comes with many SDKs, covering a wide range of languages but lacks support for Flutter. (Note that an old Dart SDK for PubNub has been created by third parties but it is using JS which is not allowed in Flutter).
All PubNub SDKs are using their underlying HTTP low level protocol. The best approach for our Flutter SDK would have been writing Dart code making use of the HTTP layer directly but:
The approach we took was to wrap a few functionalities exposed by the iOS and Android SDK and expose everything through streams.
The project for creating such SDK took about 5 days and has all the required functionalities for supporting a real-time chat application in Flutter.
The plugin was written in Objective C and Java as first-class citizen in the PubNub mobile SDK. I had started to write a Swift and Kotlin based plugin but rapidly hit a major roadblock due to the PubNub SDK dependency. After spending about 4 hours on trying to figure out why the Swift plugin was not recognizing the PubNub SDK, I reverted back to the previous languages. Also looking at all the official Flutter plugins, most of them are written in the older languages so I followed the principle: it ain’t broke, don’t fix it.
In this article, I will mainly highlight some of the challenges I faced during development. I will then describe how the plugin works. I am also looking at open sourcing the plugin and will submit an update when I do. My hope in the future is to have a pure dart/flutter plugin which could then run also in Flutter Web and Desktop applications. Maybe PubNub will create such plugin and officially support it.
First, we had to import the PubNub SDK into both and iOS projects in the plugin. iOS setup relies on CocoaPods and Android, Gradle.
iOS changes must be edited in the podspec file and NOT the Podfile.
Below shows where things are on iOS in relation to the podspec file and the actual source code for the plugin:
Open the podspec file and add one line for PubNub dependency as shown below:
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'pubnub_flutter'
s.version = '0.0.1'
s.summary = 'A new flutter plugin project.'
s.description = <<-DESC
A new flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => '[email protected]' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.dependency 'PubNub'
s.ios.deployment_target = '8.0'
end
On Android, the gradle file for the plugin (pubnub_flutter) module must be edited and reference the PubNub SDK:
Open the correct build.gradle file and add the PubNub dependencies as shown below:
group 'com.example.pubnubflutter'
version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 27
defaultConfig {
minSdkVersion 16
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
dependencies {
implementation 'com.pubnub:pubnub-gson:4.21.0'
}
}
Some official Flutter plugins, namely Sensors and Battery, show how streams . can be handled but only from one source (sensor or battery). In our case, PubNub has many callbacks/delegates for capturing Presence, Status and Messages changes. Furthermore, in our plugin architecture, we wanted to separate all these events into specialized streams, making it easier to listen and react to specific incoming events.
We will take the Java plugin as an example for the rest of this article.
First, we define and initialize our streams as follow:
public class PubnubFlutterPlugin implements MethodCallHandler {
private static final String PUBNUB_FLUTTER_CHANNEL_NAME =
"flutter.ingenio.com/pubnub_flutter";
private static final String PUBNUB_MESSAGE_CHANNEL_NAME =
"flutter.ingenio.com/pubnub_message";
private static final String PUBNUB_STATUS_CHANNEL_NAME =
"flutter.ingenio.com/pubnub_status";
private static final String PUBNUB_PRESENCE_CHANNEL_NAME =
"flutter.ingenio.com/pubnub_presence";
private static final String PUBNUB_ERROR_CHANNEL_NAME =
"flutter.ingenio.com/pubnub_error";
private MessageStreamHandler messageStreamHandler;
private StatusStreamHandler statusStreamHandler;
private ErrorStreamHandler errorStreamHandler;
private PresenceStreamHandler presenceStreamHandler;
private PubnubFlutterPlugin() {
messageStreamHandler = new MessageStreamHandler();
statusStreamHandler = new StatusStreamHandler();
errorStreamHandler = new ErrorStreamHandler();
presenceStreamHandler = new PresenceStreamHandler();
}
The first channel: PUBNUB_FLUTTER_CHANNEL_NAME is the Method channel for handling method calls. All other channels are Event channels for handling events.
We then create the channels and attach the streams them:
/**
* Plugin registration.
*/
public static void registerWith(Registrar registrar) {
PubnubFlutterPlugin instance = new PubnubFlutterPlugin();
final MethodChannel channel = new MethodChannel(registrar.messenger(), PUBNUB_FLUTTER_CHANNEL_NAME);
channel.setMethodCallHandler(instance);
final EventChannel messageChannel =
new EventChannel(registrar.messenger(), PUBNUB_MESSAGE_CHANNEL_NAME);
messageChannel.setStreamHandler(instance.messageStreamHandler);
final EventChannel statusChannel =
new EventChannel(registrar.messenger(), PUBNUB_STATUS_CHANNEL_NAME);
statusChannel.setStreamHandler(instance.statusStreamHandler);
final EventChannel presenceChannel =
new EventChannel(registrar.messenger(), PUBNUB_PRESENCE_CHANNEL_NAME);
presenceChannel.setStreamHandler(instance.presenceStreamHandler);
final EventChannel errorChannel =
new EventChannel(registrar.messenger(), PUBNUB_ERROR_CHANNEL_NAME);
errorChannel.setStreamHandler(instance.errorStreamHandler);
}
The handling of method calls needs to make use of 3 methods:
Flutter documents the data types that can be used in Plugins:
As we defined multiple streams, one per. specific real-time messaging use cases (status, presence, messages and errors), we have defined, as described earlier, specific classes.
Each of these streams implements the EventChannel.StreamHandler interface. Each class handles listening and cancelling the stream, these are callbacks handled by the StreamHandler. We are therefore creating a base class that handles this for us:
public abstract static class BaseStreamHandler implements EventChannel.StreamHandler {
private EventChannel.EventSink sink;
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
this.sink = eventSink;
}
@Override
public void onCancel(Object o) {
this.sink = null;
}
}
Then each of or Stream Handlers extend such class, benefiting of the Stream lifecycle events. The following shows how the messaging Stream Handler is implemented. Every time a message is received on subscribed PubNub channels, the Dart layer receives such messages via a dedicated message stream:
public static class MessageStreamHandler extends BaseStreamHandler {
void sendMessage(PNMessageResult message) {
if (super.sink != null) {
Map<String, String> map = new HashMap<>();
map.put("uuid", message.getPublisher());
map.put("channel", message.getChannel());
map.put("message", message.getMessage().toString());
// Send message
super.sink.success(map);
}
}
}
The Method Channel for handling incoming method calls from the Dart layer needs to be connected to actions that will then trigger operations defined in the Stream Handlers.
We will continue on the messaging use case:
In the Flutter defined handler for managing incoming method calls, public void onMethodCall(MethodCall call, Result result), the publication of message is handled the following way:
case "publish":
if (handlePublish(call)) {
result.success(true);
} else {
result.error("ERROR", "Cannot Publish.", null);
}
break;
The handlePublish is responsible for extracting the arguments and sending the message to the PubNub channel:
private boolean handlePublish(MethodCall call) {
String channel = call.argument("channel");
Map message = call.argument("message");
Map metadata = call.argument("metadata");
if(client != null && channel != null && message != null) {
client.publish().channel(channel).message(message).meta(metadata).async(new PNCallback<PNPublishResult>() {
@Override
public void onResponse(PNPublishResult result, PNStatus status) {
handleStatus(status);
}
});
return true;
}
return false;
}
Note that every PubNub operation triggers a callback and passes a status related to the operation. Such status is handled by handleStatus(status) which makes use of the StatusStreamHandler.
private void handleStatus(PNStatus status) {
if(status.isError()) {
Map<String, Object> map = new HashMap<>();
map.put("operation", PubnubFlutterPlugin.getOperationAsNumber(status.getOperation()));
map.put("error", status.getErrorData().toString());
errorStreamHandler.sendError(map);
} else {
statusStreamHandler.sendStatus(status);
}
}
If you implement a plugin, manage it in its own GitHub repo. From your app viewpoint, Flutter makes it easy to import the plugin:
In your pubspec.yaml, just reference the plugin as:
pubnub_flutter:
git:
url: https://github.com/Ingenio/pubnub_flutter
ref: 1f7965b
Note you should do the same if you fork a repo, very useful especially if you are waiting for a PR to be approved by the original plugin, package component.
Writing the plugin was not difficult. However, in order to do this, some knowledge of iOS and Android and associated languages must be understood.
Wrapping an SDK that does not have to be mobile specific as we did here is not optimal but saves time.
As Flutter grows, I hope 3rd parties would pay attention and implement Dart/Flutter SDKs, removing the need to handle and manage complex code.
✅ Further reading:
☞ The differences between Flutter vs React Native
☞ React Native vs Flutter (Which is best for startup ?)
☞ Flutter & Dart - The Complete Flutter App Development Course
☞ Flutter TabBar & TabBarView by Sample Code | Flutter Tutorial | Flutter 2023
☞ Flutter Tutorial: Flutter PDF Viewer | Flutter PDF Tutorial | PDF in Flutter
☞ Flutter Tutorial For Beginners In 1 Hour
☞ Flutter Course - Full Tutorial for Beginners
☞ Code a Twitter Clone with Flutter, Appwrite, Riverpod | Full Tutorial for Beginners to Advanced
☞ Flutter File Upload - Pick, Crop, and Save Images to the Cloud