Flutter Plugins in the real world

Olivier Brand
7 min readDec 3, 2018

--

Introduction

With the introduction of Google’s Cross Platform, Flutter, development of apps can tremendously be accelerated. However, the sticky point, as in any Cross Platform environment lies in being able to access native platform features such as Camera, Bluetooth… All of these can be accessed via Plugins, exposing iOS and Android native code to Flutter applications.

Many articles cover pros and cons related to Flutter. A recurring cons revolves around the platform maturity and the lack of support of 3rd party frameworks (e.g. analytics) that are essential for launching a production application.

This article is not intended to compare Flutter with other environments such as Xamarin, React Native, PhoneGap and others, but rather share my experience with Flutter and help you decide if such framework can be used for your production apps or not.

What is the real issue?

Within most organizations, Marketing and Product teams usually put tools in place for maximizing the success of mobile applications. These tools range from analytics, marketing, user testing and many others. As Flutter is new and also uses Dart as language, chances to find any third party support are very slim. This is where plugins come to the rescue.

Approaches

There are 2 approaches to supporting a third party service:

  • Dart packages: General packages written in Dart. Some of these may contain Flutter specific functionality and thus have a dependency on the Flutter framework, restricting their use to Flutter only. It should be noted that some Dart packages are not compatible with Flutter, especially some that require the use of Javascript that cannot be used in Flutter.
  • Plugin packages: A specialized Dart package which contain an API written in Dart code combined with a platform-specific implementation for Android (using Java or Kotlin), and/or for iOS (using ObjC or Swift).

Choice for writing a Dart package or a Plugin package will be dictated by how third parties expose their capability. Such as:

  • 3rd party exposes a REST API: a Dart Package can be written and be platform agnostic. However, in some occasions and depending on level of efforts for wrapping a REST API, a plugin package approach that wraps the third party iOS and Android could be preferred.
  • 3rd party exposes an iOS and Android native SDK and possibly a REST API: a Plugin Package can be implemented, requiring writing native code for wrapping specific iOS and Android capabilities to be used at the Flutter level.

The rest of the article will focus on Plugin Packages as these would usually answer issues that most developers would encounter when integrating 3rd party services.

Plugin Packages to the rescue

Starting point

Plugin packages are easy to write. Flutter provides tools for helping developers to start writing plugins. The best way to start writing a plugin is to use the command line tool:

flutter create --template=plugin my_plugin

Or of Swift or Kotlin were to be used:

flutter create --template=plugin -i swift -a kotlin my_plugin

Other options, such as package name can also be passed. For more info: flutter.io

The command generates a complete example. The generated plugin project contains:

  • The Flutter Plugin: located in the lib folder.
  • The Android plugin: located in the android/src/main/java folder.
  • The iOS plugin: located in the iOS/Classes folder.
  • Example Flutter app making use of the Flutter plugin class: example/lib/main.dart

The example app can be executed via Android studio, XCode, from the native app side (Android or iOS projects) or via Android Studio, from the flutter app side (Flutter project).

Wrapping the 3rd party library

For our needs, we had the requirement to use a 3rd party service for our real time chat and push notifications capabilities. The service is provided by PubNub (https://www.pubnub.com). PubNub provides many SDKs for easing the integration with various platforms. The core integration, it’s lowest level, is provided through a REST API, utilizing long polling techniques for subscribing to channels, presence and a simple API for publishing messages, states…

As REST can be directly use in Flutter, the best approach would be to build a Flutter package, wrapping the functionalities that we need. However, wrapping the REST API would fall into writing a robust PubNub SDK, managing disconnections, error handling,… something that would fall more onto PubNub to provide such features.

As iOS and Android already wraps the PubNub REST API, gracefully handling long polling, errors, connectivity issues, we will wrap specific functionalities writing a Flutter Package Plugin using a minimal amount of iOS and Android code.

The Plugin

Most examples found out there cover some basic plugin functionalities. In our case, our requirements are:

  • Configure the plugin with the subscribe and publish key
  • Subscribe to one or more channels, passing an optional UUID for client tracking and message reconciliation purposes.
  • Unsubscribe from a channel
  • Publish messages
  • Listen to status changes (connections, subscriptions,…)
  • Listen to messages

Such requirements make use of the 2 Flutter plugin framework mechanisms:

  • Method Channels: ability to call a native function, passing optional arguments and receiving results in a synchronous way. The following functionalities listed above are covered by this channel type: configure, subscribe, unsubscribe and publish
  • Event Channels: ability to listen and receive data from a data stream. The following functionalities listed above are covered by this channel type: listen to status changes and messages.

This article will not go in full details on how to use these APIs but rather focus on a couple examples: call a method and listen to more than one stream.

Method Channel

First we need to define a Flutter variable in the flutter plugin for making calls to the native layer:

MethodChannel _channel;

We then need to initialize such channel. In Flutter, any channels needs to have a unique name.

_channel = MethodChannel('pubnub_flutter');

Then define arguments and pass them:

var args = {"publishKey": publishKey, "subscribeKey": subscribeKey};
if(uuid != null) {
args["uuid"] = uuid;
}
_channel.invokeMethod('create', args);

For initialization point of view, a good way to handle this through a plugin is to do this via the plugin constructor. The entire code is as:

PubNubFlutter(String publishKey, String subscribeKey, {String uuid}) {
_channel = MethodChannel('pubnub_flutter');
_messageChannel = const EventChannel('plugins.flutter.io/pubnub_message');
_statusChannel = const EventChannel('plugins.flutter.io/pubnub_status');
var args = {"publishKey": publishKey, "subscribeKey": subscribeKey};
if(uuid != null) {
args["uuid"] = uuid;
}
_channel.invokeMethod('create', args);
}

Code on iOS for the create method is:

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {    if  ([@"create" isEqualToString:call.method]) {        result([self handleCreate:call]);    } else {        result(FlutterMethodNotImplemented);
}
}

The handling of arguments and return is:

- (id) handleCreate:(FlutterMethodCall*)call {    NSString *publishKey = call.arguments[@"publishKey"];    NSString *subscribeKey = call.arguments[@"subscribeKey"];    NSString *uuid = call.arguments[@"uuid"];    if(publishKey && subscribeKey) {               self.config =        [PNConfiguration configurationWithPublishKey:publishKey                                        subscribeKey:subscribeKey];        self.config.stripMobilePayload = NO;        if(uuid) {            self.config.uuid = uuid;        } else {            self.config.uuid = [NSUUID UUID].UUIDString.lowercaseString;        }        self.client = [PubNub clientWithConfiguration:self.config];        [self.client addListener:self];    }    // Nothing to return, just return NULL    return NULL;}

Event Channel

We will be looking here at the more realistic scenario for handling multiple streams, one for listening to message and one to status changes. This scenario lacks complete examples and tutorials on web, developers would have to look into some published plugins on GitHub and make sense of it.

/// Fires whenever the a message is received.
Stream<Map> get onMessageReceived {
if (_onMessageReceived == null) {
_onMessageReceived = _messageChannel
.receiveBroadcastStream()
.map((dynamic event) => _parseMessage(event));
}
return _onMessageReceived;
}
/// Fires whenever the status changes.
Stream<PubNubStatus> get onStatusReceived {
if (_onStatusReceived == null) {
_onStatusReceived = _statusChannel
.receiveBroadcastStream()
.map((dynamic event) => _parseStatus(event));
}
return _onStatusReceived;
}

Note that the 2 channels are created in the constructor defined in the Method Channel section.

On iOS we need to define one class per FlutterStreamHandler so each specific message can be channeled to a different stream in the Flutter application.

#import <Flutter/Flutter.h>#import <PubNub/PubNub.h>@class MessageStreamHandler;@class StatusStreamHandler;@interface PubnubFlutterPlugin : NSObject<FlutterPlugin>@property (nonatomic, strong) MessageStreamHandler *messageStreamHandler;@property (nonatomic, strong) StatusStreamHandler *statusStreamHandler;
@end@interface MessageStreamHandler : NSObject<FlutterStreamHandler>@property (nonatomic, strong) FlutterEventSink eventSink;- (void) sendMessage:(PNMessageResult *)message;@end@interface StatusStreamHandler : NSObject <FlutterStreamHandler>@property (nonatomic, strong) FlutterEventSink eventSink;- (void) sendStatus:(PNStatus *)status;@end

In the .m file, implementation of the plugin registration method is:

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {    FlutterMethodChannel* channel = [FlutterMethodChannel                                     methodChannelWithName:@"pubnub_flutter"                                     binaryMessenger:[registrar messenger]];    PubnubFlutterPlugin* instance = [[PubnubFlutterPlugin alloc] init];    [registrar addMethodCallDelegate:instance channel:channel];    // Event channel for streams    instance.messageStreamHandler = [MessageStreamHandler new];    FlutterEventChannel* messageChannel =    [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/pubnub_message"                              binaryMessenger:[registrar messenger]];    [messageChannel setStreamHandler:instance.messageStreamHandler];    // Event channel for streams    instance.statusStreamHandler = [StatusStreamHandler new];    FlutterEventChannel* statusChannel =    [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/pubnub_status"                              binaryMessenger:[registrar messenger]];    [statusChannel setStreamHandler:instance.statusStreamHandler];    instance.errorStreamHandler = [ErrorStreamHandler new];
}

Implementation of the stream handler classes is:

@implementation MessageStreamHandler- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {    self.eventSink = eventSink;    return nil;}- (FlutterError*)onCancelWithArguments:(id)arguments {    self.eventSink = nil;    return nil;}- (void) sendMessage:(PNMessageResult *)message {     if(self.eventSink) {         NSDictionary *result = @{@"uuid": message.uuid, @"channel": message.data.channel, @"message": message.data.message};         self.eventSink(result);     }}@end@implementation StatusStreamHandler- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {    self.eventSink = eventSink;    return nil;}- (FlutterError*)onCancelWithArguments:(id)arguments {    self.eventSink = nil;    return nil;}- (void) sendStatus:(PNStatus *)status {    if(self.eventSink) {        self.eventSink(status.stringifiedOperation);    }}@end

In order to send data through these channels, simple calls need to be made. For example, sending a message would be:

[self.messageStreamHandler sendMessage:message];

And sending a status would be:

[self.statusStreamHandler sendStatus:status];

Putting it all together

Flutter provides a way to submit a plugin to the centralized plugin repo. However, not all plugins should be made available to the public. Flutter provides an easy way to reference a plugin in another project and is done via dependencies in the pubspec.yaml file:

dependencies:
pubnub_flutter:
path: ../pubnub_flutter

Once the plugin available, using our plugin is as easy as:

import 'package:flutter/material.dart';import 'package:pubnub_flutter/pubnub_flutter.dart';void main() => runApp(MyApp());class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
PubNubFlutter _pubNubFlutter;
@override
void initState() {
super.initState();
_pubNubFlutter = PubNubFlutter("pubkey", "subkey");
_pubNubFlutter.uuid().then((uuid) => print('UUID: ${uuid}'));
_pubNubFlutter.onStatusReceived.listen((status) {
print("Status:${status.toString()}");
});
_pubNubFlutter.onMessageReceived.listen((message) {
print("Message:${message}");
});
_pubNubFlutter.onErrorReceived.listen((error) {
print("Error:${error}");
});
}
@overrideWidget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('PubNub'),
),
body: Center(
child:Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children:<Widget>[
FlatButton(color: Colors.black12,onPressed: () {_pubNubFlutter.unsubscribe(channel: "my_channel");},
child: Text("Unsubscribe")),
FlatButton(color: Colors.black12,onPressed: () {_pubNubFlutter.subscribe(["my_channel"]);},
child: Text("Subscribe"))
])
],)
),
),
);
}
}

Conclusion

Implementing plugins was a critical aspect for ensuring any third party services could be integrated inside a Flutter app. Plugins can be written to encapsulate existing iOS or Android libraries or make use of available REST APIs when these exist via Flutter packages.

Other frameworks such as React Native, PhoneGap have a similar framework for writing plugins, so any developer that has Cross Platform experience will feel at ease with Flutter.

--

--

Olivier Brand

I am passionate about mobile, especially designing end to end processes for delivering solid apps and supporting backend systems.