All Blog Posts

A Guide to Developing a PubNub Plugin for Flutter

Having recently developed a Flutter app and realising the language is not supported by PubNub, I developed my own PubNub plugin for Flutter, here's how you can do the same.

Development

Developing a PubNub Plugin for Flutter

Flutter is a portable UI kit from Google which lets developers build native applications for mobile and web from a single codebase. It’s used to build Android and iOS apps, as well as being the primary language for creating applications on Google Fuchsia.

When it comes to cross-platform development languages, such as Flutter and React-Native, accessing native features such as camera, Bluetooth or third-party functionality requires a bit of “hacking”. For native features, we can use the plugins, but for third-party systems we need to write our own package.

Check out my PubNub plugin for Flutter on GitHub, or read on for a full guide to developing your own.

 

Jump to:

Two approaches to developing Flutter packages.

Getting started & setting up a Flutter plugin.

What is PubNub and what is it used for?

How to develop your own PubNub plugin for Flutter:

1. Importing the PubNub SDK.

2. Linking native Android code to Flutter.

3. Publishing the PubNub plugin.

Two approaches to developing Flutter packages

Selecting which method to go with depends entirely on the way the third party works.

  1. Dart Packages: These use the Dart programming language and contain Flutter-specific functionality; meaning that the packages are completely reliant on the Flutter framework.
  2. Plugin Packages: These packages contain an API written in Dart, combined with platform-specific implementation for Android and iOS, which use Java, Kotlin, Objective-C or Swift.

Plugin packages are not hard to write, Flutter provides a sample for getting a platform’s version as a guideline. Flutter is able to do almost anything that a native app can do through the use of Platform Channels and Message Passing.

The flow is Flutter instructing the native iOS/Android code to perform an action and then waiting to get the result back to Dart. In the directory structure of a Flutter app, you’d notice that along with the lib directory which contains Dart code, there is also an Android and iOS directory.

These contain the iOS and Android codebases, which are native projects that will run the compiled Dart code. By using the MethodChannel, we can establish communication between the Flutter (Dart) and the native Android/iOS code.

In this folder, we can also see the example Flutter code where we can test our Flutter plugin within an app.

Flutter and Native Android iOS Code

A diagram explaining how PubNub integrates and communicates with native Android and iOS code. Source: Creating a Bridge in Flutter.

Getting started and setting up a Flutter plugin

Start a Flutter project->Flutter Plugin and set the name and package of the plugin you want to create. Android Studio will generate an example plugin for you. This plugin retrieves the platform’s version and displays it within the interface. Let’s go step-by-step and see what every file does.

In the flutter_plugin.dart file of the lib folder:

class FlutterPluginrrererw {

static const MethodChannel _channel =  const MethodChannel(‘flutter_plugin’);

static Future<String> get platformVersion async {

  String version = await _channel.invokeMethod(‘getPlatformVersion’);

  return version;

}

}

FlutterPlugin.java file of the android folder:

/** FlutterPlugin */

public class FlutterPluginrrererwPlugin implements MethodCallHandler {

/** Plugin registration. */

public static void registerWith(Registrar registrar) {

  final MethodChannel channel = new MethodChannel(registrar.messenger(), “flutter_plugin”);

  channel.setMethodCallHandler(new FlutterPluginrrererwPlugin());

}

@Override  public void onMethodCall(MethodCall call, Result result) {

  if (call.method.equals(“getPlatformVersion”))
{

    result.success(“Android ” + android.os.Build.VERSION.RELEASE);

}

else {

    result.notImplemented();

  }

}

}

We first need to create a MethodChannel variable, which will allow us to communicate with the native code. The initialisation of the MethodChannel requires a name, be careful and make sure that name is the same on the native Android/iOS code, which is located in representative folders (e.g., for Android will be android->src->main->your_package_name->flutter_plugin).

The get platformVersion is an async method that calls MethodChannel’s invokeMethod. This method call will trigger the onMethodCall on the native code. On the onMethodCall function we need to handle the “getPlatformVersion” method, by implementing its functionality and returning the result of it. Notice, the result variable should be the same type of the return value on the get platformVersion on flutter_plugin.dart.

We return only standard types of variables on the result.success, we could also return custom variables but we would have to implement the variable in native (Android or iOS) and Flutter (Dart) code in order to be able to parse the custom variable.

After we have created the link between the Flutter and the native code, we can use this functionality on the example app.

The main.dart file of the example folder:

import ‘package:flutter/material.dart’;

import ‘dart:async’;

import ‘package:flutter/services.dart’;
//Import the plugin

import ‘package:flutter_plugin/flutter_plugin.dart’;

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {

@override MyAppState createState() => MyAppState();

}

class MyAppState extends State<MyApp>

{

String _platformVersion = ‘Unknown’;

@override  void initState()

{

  super.initState();

  initPlatformState();

}

// Platform messages are asynchronous, so we initialize in an async method.

Future<void> initPlatformState() async {

  String platformVersion;

  // Platform messages may fail, so we use a try/catch PlatformException.

  try {

//Call the plugin

    platformVersion = await FlutterPlugin.platformVersion;

  } on PlatformException {

    platformVersion = ‘Failed to get platform version.’;

  }

  // If the widget was removed from the tree while the asynchronous platform

  // message was in flight, we want to discard the reply rather than calling

  // setState to update our non-existent appearance.

  if (!mounted) return;

  setState(() {

    _platformVersion = platformVersion;

  });

}

@override Widget build(BuildContext context) {

  return MaterialApp(

    home: Scaffold(

      appBar: AppBar(

        title: const Text(‘Plugin example app’),

      ),

      body: Center(

        child: Text(‘Running on: $_platformVersion\n),

      ),

    ),

  );

}

}

What is PubNub and what can it be used for?

In one of the team’s latest Innovation Time projects; an Alexa Skill that could teach children how to read using phonics, I tried to connect Alexa with a mobile app so they could work together in real time.

In order for the app to know which page is being read or on what state (enabled/disabled) Alexa is, we need a way to send these signals/messages to the app. The best way, to our knowledge, was to use the PubNub API.

PubNub is a real-time message distributer framework that supports a tonne of languages (Java, Javascript, PHP, Python, Swift, Ruby and many others).

How Does PubNub Work?

Source: pubnub.com/what-we-do.

Despite the fact that they support so many languages, they do not, as of yet, support Flutter. So we have to implement that functionality, either by writing a Dart SDK or we could use the Android/iOS SDK as a Flutter plugin. Due to time constraints and because the SDKs are very robust and reliable, we choose to make a Flutter Plugin.

How to develop your own PubNub plugin for Flutter

Initially, we need to create an example plugin that Android studio generates (we can then delete the unwanted functionality) with the name PubNubPlugin. Moreover, we will need to publish, subscribe and use a secret key in order to initialise the PubNub channel. We can do that through the PubNub website under a free account. In the PubNub dashboard we can then create an app.

Developing a PubNub Plugin

1. Importing the PubNub SDK

The first step is to import the PubNub SDK for Android projects (at this point we need to say that during this week, we were focused mainly on the Android platform, but we will implement the iOS plugin, too). You can import the SDK in the same way as we would do that if the app was native, on the build.gradle of the Android folder.

android {

…..

…..

dependencies {

//PubNub pSDK

 implementation group: ‘com.pubnub’, name: ‘pubnub-gson’, version: ‘4.22.0-beta’
//We need Gson, to parse the json objects

 implementation ‘com.google.code.gson:gson:2.8.5’

// We used this joda, for the timestamps of the messages but that is optional

 implementation group: ‘joda-time’, name: ‘joda-time’, version: ‘2.9.4’

}

}

So now, we can implement all the Android PubNub functionality, in the PubNubPlugin.java.

We will need four functions:

  1. Create a PubNub channel
  2. Subscribe to a channel
  3. Unsubscribe from a channel
  4. Send a message to the channel

private void createChannel(MethodCall call, Result result)

{

 String publishKey = call.argument(“publishKey”);

 String subscribeKey = call.argument(“subscribeKey”);

 String secretKey = call.argument(“secretKey”);

 uuid = java.util.UUID.randomUUID().toString();

 Log.d(getClass().getName(), “Create pubnub with publishKey ” + publishKey + “, subscribeKey ” + subscribeKey + ” uuid” + uuid);

 if ((publishKey != null && !publishKey.isEmpty()) && (subscribeKey != null && !subscribeKey.isEmpty()))

 {

    PNConfiguration pnConfiguration = new PNConfiguration();

    pnConfiguration.setPublishKey(publishKey);

    pnConfiguration.setSubscribeKey(subscribeKey);

    pnConfiguration.setUuid(uuid);

    pnConfiguration.setSecretKey(secretKey);

    pnConfiguration.setSecure(true);

    pnConfiguration.setLogVerbosity(PNLogVerbosity.BODY);

    pubnub = new PubNub(pnConfiguration);

    Log.d(getClass().getName(), “PubNub configuration created”);

    result.success(“PubNub configuration created”);

 }

 else

 {

    Log.d(getClass().getName(), “Keys should not be null”);

    result.success(“Keys should not be null”);

 }

}

 

private void subscribeToChannel(MethodCall call, final Result result)

{

 /* Subscribe to the demo_tutorial channel */

 channelName = call.argument(“channelName”);

 

 Log.d(getClass().getName(), “Attempt to Subscribe to channel: ” + channelName);

 try

 {

    pubnub.addListener(new SubscribeCallback()

    {

       @Override public void status(PubNub pubnub, PNStatus status)

       {

          if (status.getCategory() == PNStatusCategory.PNConnectedCategory)

          {

             Log.d(getClass().getName(), “Subscription was successful at channel ” + channelName);

             statusSender.success(“Subscription was successful at channel ” + channelName);

             messageSender.success(“Ready to listen messages”);

             result.success(true);

          }

          else

          {

             Log.d(getClass().getName(), “Subscription failed at channe l” + channelName);

             Log.d(getClass().getName(), status.getErrorData().getInformation());

             statusSender.success(“Subscription failed at channel ” + channelName + “‘\n+ status.getErrorData().getInformation());

             result.success(false);

          }

       }

 

       @Override public void message(PubNub pubnub, PNMessageResult message)

       {

          //If is not your message

          if (message != null && message.getPublisher().compareToIgnoreCase(uuid) != 0)

          {

             try

             {

                Message receivedMessage = new Gson().fromJson(message.getMessage(), Message.class);

                receivedMessage.setChannel(message.getChannel());

                receivedMessage.setPublisher(message.getPublisher());

                messageSender.success(receivedMessage.getMessage());

             }

             catch (Exception e)

             {

                messageSender.success(“Failed to parse message”);

                e.printStackTrace();

             }

          }

       }

       @Override public void presence(PubNub pubnub, PNPresenceEventResult presence)

       {

          Log.d(getClass().getName(), “Presence: getChannel ” + presence.getChannel() + “getEvent ” + presence.getEvent() + “getSubscription ” + presence.getSubscription() + “getUuid ” + presence.getUuid());

       }

    });

 

    pubnub.subscribe().channels(Arrays.asList(channelName)).execute();

 }

 catch (Exception e)

 {

    Log.d(getClass().getName(), e.getMessage());

    result.success(false);

 }

}

private void unSubscribeFromChannel(MethodCall call, final Result result)

{

 channelName = call.argument(“channelName”);

 Log.d(getClass().getName(), “Attempt to Unsubscribe to channel: ” + channelName);

 try

 {

    pubnub.addListener(new SubscribeCallback()

    {

       @Override public void status(PubNub pubnub, PNStatus status)

       {

          if (status.getCategory() == PNStatusCategory.PNDisconnectedCategory)

          {

             Log.d(getClass().getName(), “Unsubscribe successfully”);

             result.success(true);

          }

          else

          {

             Log.d(getClass().getName(), “Unsubscribe failed”);

             Log.d(getClass().getName(), status.getErrorData().getInformation());

             result.success(false);

          }

       }

       @Override public void message(PubNub pubnub, PNMessageResult message)

       {

          try

          {

             Message receivedMessage = new Gson().fromJson(message.getMessage(), Message.class);

             receivedMessage.setChannel(message.getChannel());

             receivedMessage.setPublisher(message.getPublisher());

             messageSender.success(receivedMessage.getMessage());

          }

          catch (Exception e)

          {

             messageSender.success(“Failed to parse unsubscribe message”);

             e.printStackTrace();

          }

       }

       @Override public void presence(PubNub pubnub, PNPresenceEventResult presence)

       {

          Log.d(getClass().getName(), “Presence: getChannel ” + presence.getChannel() + “getEvent ” + presence.getEvent() + “getSubscription ” + presence.getSubscription() + “getUuid ” + presence.getUuid());

       }

    });

 

    pubnub.unsubscribe().channels(Arrays.asList(channelName)).execute();

 }

 catch (Exception e)

 {

    Log.d(getClass().getName(), e.getMessage());

    result.success(false);

 }

}

private void sendMessageToChannel(MethodCall call, final Result result)

{

 HashMap<String, String> message = new HashMap<>();

 message.put(“sender”, (String)call.argument(“sender”));

 message.put(“message”, (String)call.argument(“message”));

 message.put(“timestamp”, DateTimeUtil.getTimeStampUtc());

 pubnub.publish().channel(channelName).message(message).async(

    new PNCallback()

    {

       @Override public void onResponse(Object object, PNStatus status)

       {

          try

          {

             if (!status.isError())

             {

                Log.v(getClass().getName(), “publish(” + object + “)”);

                result.success(true);

             }

             else

             {

                Log.v(getClass().getName(), “publishErr(” + status.getErrorData() + “)”);

                result.success(false);

             }

          }

          catch (Exception e)

          {

             e.printStackTrace();

             result.success(false);

          }

       }

    }

 );

}

2. Linking native code to Flutter

So far we have implemented the native Android functions, now we have to link them with Flutter code. In order to do that we will use the onMethodCall function on PubnubPlugin.java.

@Override public void onMethodCall(MethodCall call, Result result)

{

 switch (call.method)

 {

    case “create”:

       createChannel(call, result);

       break;

    case “subscribe”:

       subscribeToChannel(call, result);

       break;

    case “unsubscribe”:

       unSubscribeFromChannel(call, result);

       break;

    case “message”:

       sendMessageToChannel(call, result);

       break;

    default:

       result.notImplemented();

 }

}

And because we will need to send messages and the status of PubNub on the Flutter app, we will need to create EventChannel objects that will carry those messages. Some of you may think, why are we not using the result.success to pass the result back to the Flutter app? The reason being that the result.success can be called only once, but we will retrieve many messages.

In the pubnub_plugin.dart we will need to call PubnubPlugin.java’s methods using the MethodChannel.

PubnubPlugin(String publishKey, String subscribeKey, String secretKey) {

var args = {

  ‘publishKey’: publishKey,

  ‘subscribeKey’: subscribeKey,

  ‘secretKey’: secretKey,

};

channelPubNub.invokeMethod(‘create’, args);

}

unsubscribe(String channel)  {

Object result = new Object();

var args = {

  ‘channelName’: channel

};

if (channelPubNub != null) {

   channelPubNub.invokeMethod(‘unsubscribe’, args);

}

else {

  new NullThrownError();

}

}

subscribe(String channel) {

var args = {

  ‘channelName’: channel

};

if (channelPubNub != null) {

  channelPubNub.invokeMethod(‘subscribe’, args);

}

else {

  new NullThrownError();

}

}

sendMessage(String message) {

var args = {

  ‘sender’: ‘Flutter’,

  ‘message’: message

};

if (channelPubNub != null) {

  channelPubNub.invokeMethod(‘message’, args);

}

else {

  new NullThrownError();

}

}

Now we can use all these functions on the main.dart file of the Flutter example:

import ‘package:flutter/material.dart’;

import ‘dart:async’;

import ‘package:flutter/services.dart’;

import ‘package:pubnub_plugin/pubnub_plugin.dart’;

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {

@override MyAppState createState() => MyAppState();

}

class MyAppState extends State<MyApp> {

PubnubPlugin _pubNubFlutter;

String receivedStatus = ‘Status: Unknown’;

String receivedMessage = ;

String sendMessage = ;

@override

void initState() {

  super.initState();

  _pubNubFlutter = PubnubPlugin(“pub-c-123”, “sub-c-123”, “sec-c-123”);

  _pubNubFlutter.onStatusReceived.listen((status) {

    setState(() {

      receivedStatus = status;

    });

  });

  _pubNubFlutter.onMessageReceived.listen((message) {

    setState(() {

      receivedMessage = message;

    });

  });

}

@override Widget build(BuildContext context) {

  return MaterialApp(

    home: Scaffold(

      appBar: AppBar(

        title: Text(‘PubNub’),

      ),

      body: Center(

        child: Column(

          mainAxisAlignment: MainAxisAlignment.center,

          children: <Widget>[

            new Expanded(

              child: new Text(

                receivedStatus,

                style: new TextStyle(color: Colors.black45),

              ),

            ),

            new Expanded(

              child: new Text(

                receivedMessage,

                style: new TextStyle(color: Colors.black45),

              ),

            ),

            TextField(

              maxLength: 80,

              onChanged: (text){

                sendMessage = text;

              },

              decoration: InputDecoration(

                border: OutlineInputBorder(),

                hintText: “Message to send”,

                hintStyle: TextStyle(fontWeight: FontWeight.w300, color: Colors.grey)

              ),

              style: TextStyle(color: Colors.black, fontWeight: FontWeight.w300),

            ),

            Row(

              mainAxisAlignment: MainAxisAlignment.spaceEvenly,

              children: <Widget>[

                FlatButton(color: Colors.black12, onPressed: () {

                  _pubNubFlutter.unsubscribe(“phonics”);

                },

                  child: Text(“Unsubscribe”)),

                FlatButton(color: Colors.black12, onPressed: () {

                  _pubNubFlutter.subscribe(“phonics”);

                },

                  child: Text(“Subscribe”))

              ]),

            FlatButton(color: Colors.black12, onPressed: () {

              _pubNubFlutter.sendMessage(sendMessage);

            },

              child: Text(“Send Message”)),

          ],)

      ),

    ),

  );

}

}

3) Publishing your PubNub plugin

Instead of using the plugin locally, we could also publish it to PubDev Dart Packages, and use it as a plugin on the pubspec.yaml. The commands to publish a plugin are:

flutter packages pub publish–dry-run
flutter packages pub publish

 

Implementing plugins is crucial for ensuring your app will be available on production, because otherwise mobile app developers will have to reinvent the wheel by implementing the same API in native Dart code, which is both time and money intensive.

Other frameworks such as React-Native and PhoneGap have a similar framework for writing plugins, so any developer that has cross-platform experience will find it simple and easy to develop a Flutter plugin.

Published on May 22, 2019, last updated on May 24, 2019

Give us the seal of approval!

Like what you read? Let us know.

4

Tags

Join the team

Help us save lives and change the world...

Check Out The Roles

Latest from the blog