Real Time Messaging with Flutter

Real Time Messaging with Flutter

In the current Flutter development of the new advisor application, we had the requirement for delivering real time messages using the platform

Introduction

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:

  • We are not in the business for writing SDKs
  • Wrapping the HTTP layer is complicated and necessitate handling errors, dispatch messages and events, ….
  • Overall would have taken a long time and would have required help from the PubNub engineering team.

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.

Learnings

Import PubNub SDK

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'
    }
}

Handle Multiple Streams

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:

  • result.success(object) for sending back the object to the Dart plugin
  • result.error(string, string, object) for handling error cases and inform the. Dart plugin
  • result.notImplemented() for handling any calls that have not been implemented (as method names are defined as strings in Dart, Java and Objective-C, all these files need to be in sync in term of method names and channels names.

Data Types used in Flutter plugins

Flutter documents the data types that can be used in Plugins:

Handling the Streams

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.

  • MessageStreamHandler for handling receiving new messages
  • StatusStreamHandler for handling incoming statuses
  • PresenceStreamHandler for handling presence related events
  • ErrorStreamHandler for managing errors

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

Connecting the incoming method calls to our streams

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:

  • A message needs to be sent to a PubNub channel
  • The message is passed to the PubNub SDK
  • If the Plugin had subscribed to the same PubNub channel the message was sent to, the message will be echoed and pushed back to the caller via the MessageStreamHandler. At the same time, for every PubNub operation, status is sent back via the StatusStreamHandler.

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

Reference the plugin in your app

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.

Conclusion

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.

Flutter Tutorial for Beginners - Build Android and iOS Apps with a Flutter Framework

Build Android and iOS apps with a flutter framework


You’ll learn

  • Better understanding of flutter and it’s basic widgets
  • Develop basic flutter application for android and iOS


Thanks for watching

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading

Learn Flutter & Dart to Build iOS & Android Apps

Flutter & Dart - The Complete Flutter App Development Course

Dart and Flutter: The Complete Developer’s Guide

Flutter - Advanced Course

Flutter Tutorial - Flight List UI Example In Flutter

Flutter Tutorial for Beginners - Full Tutorial

Using Go Library in Flutter

Parsing JSON in Flutter

Flutter Tutorial - Build iOS & Android App with Google Flutter & Dart

Flutter Tutorial - Build iOS & Android App with Google Flutter & Dart

In this Flutter Tutorial for Beginners, you will learn how to build iOS & Android apps with Google Flutter and Dart

In this post, Flutter Tutorial for Beginners; you will learn how to build iOS & Android apps with Googles Flutter and Dart.

You don't need to learn Android/ Java and iOS/ Swift development to build real native mobile apps!

Flutter - a framework developed by Google - allows you to learn one language (Dart) and build beautiful native mobile apps in no time. Flutter is a SDK providing the tooling to compile that Dart code into native code and it also gives you a rich set of pre-built and pre-styled UI elements (so called widgets) which you can use to compose your user interfaces!

Let's get started in this video!

Take the full course on Udemy at a discount with the following link: http://bit.ly/2O5EaKu