How to handle 3rd-Party Apps in a plugin packages on Flutter

What’s a plugin package?

If you want to develop a package that calls into platform-specific APIs, you need to develop a plugin package.
Communication with platform-specific APIs uses MethodCannel and EventChannel.

Communication channels to platform on a plugin package

MethodCannel

MethodCannel executes the API of platform by calling the method of the host, and receives the result as the return value.

Flutter                      Host <iOS>
MethodCannel ┌───────────────┐
┌──────>│ FlutterPlugin │
│ └───────────────┘
┌──────────┐ Call │
│ Plugin │*─────────┤
│ Packages │ │ Host <Android>
└──────────┘ │ ┌────────────────┐
└──────>│ FlutterPlugin? │
└────────────────┘

Step of implements

  1. Create MethodChannel
  2. Plugin registration
  3. Receive MethodCall
  4. Callback result

Android

public class HelloPlugin implements MethodCallHandler {
  // static method.
public static void registerWith(Registrar registrar) {
// 1. Create MethodChannel
final MethodChannel channel = new MethodChannel(registrar.messenger(), "hello");
// 2. Plugin registration
HelloPlugin instance = new HelloPlugin();
channel.setMethodCallHandler(instance);
}
  @Override
public void onMethodCall(MethodCall call, Result result) {
// 3. Receive MethodCall
if (call.method.equals("getPlatformVersion")) {
// 4. Callback result
if (/*condition*/) {
// 4-1. Success
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
// 4-2. Error
result.error("ERROR", "errorMessage", null):
}
} else {
// 4-2. NotImplemented Error
result.notImplemented();
}
}
}
  • Static methods need to match public static void registerWith(Registrar registrar), maybe...

Method of the Result for callback.

  • success(Object result) … Handles a successful result.
  • error(String errorCode, String errorMessage, Object errorDetails) … Handles an error result.
  • notImplemented() … Handles a call to an unimplemented method.

iOS

@interface HelloPlugin : NSObject<FlutterPlugin>
@end
@implementation HelloPlugin
// Implemented by the iOS part of a FlutterPlugin.
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
// 1. Create MethodChannel
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"hello"
binaryMessenger:[registrar messenger]];
// 2. Plugin registration.
HelloPlugin* instance = [[HelloPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
// 3. Receive MethodCall
if ([@"getPlatformVersion" isEqualToString:call.method]) {
// 4. Callback result.
if (/*condition*/) {
// 4-1. Success
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else {
// 4-2. Error
result([FlutterError errorWithCode:@"ERROR"
message:"errorMessage"
details:nil]);
}
  } else {
// 4-2. NotImplemented Error
result(FlutterMethodNotImplemented);
}
}
@end

FlutterResult … Used for submitting a method call result back to a Flutter caller.

If you call back error or notImplemented, Flutter fire a PlatformException and you need to try-catch it.

Flutter

class Hello {
// 1. Create MethodChannel
static const MethodChannel _channel = const MethodChannel('hello');
  static Future<String> get platformVersion async {
// Platform messages may fail, so we use a try/catch PlatformException.
try {
// 2. Invoke method
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
} on PlatformException catch (e) {
debugPrint(${e});
return null;
}
}
}

EventChannel

The EventChannel registers the receiver of the broadcast storm to the host and asynchronously receives the platform event in the callback.

Host <iOS>
EventChannel ┌───────────────┐
┌──────*│ FlutterPlugin │
Flutter │ └───────────────┘
┌──────────┐ Callback │
│ Plugin │<─────────┤
│ Packages │ │ Host <Android>
└──────────┘ │ ┌────────────────┐
└──────*│ FlutterPlugin? │
└────────────────┘

Step of implements

  1. Implements method of StreamHandler
  2. Create EventChannel
  3. Plugin registration
  4. Set up an event stream
  5. Emit events to Flutter

Android

public class HelloPlugin implements EventChannel.StreamHandler {
private EventChannel.EventSink _eventSink;
  public static void registerWith(Registrar registrar) {
// 2. Create EventChannel
final EventChannel eventChannel = EventChannel(registrar.messenger(), "hello");
// 3. Plugin registration
HelloPlugin instance = new HelloPlugin();
eventChannel.setStreamHandler(instance);
}
  // 1. Implements method ofStreamHandler
/**
* Handles a request to set up an event stream.
*
* @arguments possibly null.
* @events an EventChannel.EventSink for emitting events to the Flutter receiver.
*/
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
// 4. Set up an event stream
_eventSink = events;
}
  /**
* Handles a request to tear down the most recently created event stream.
*
* @arguments possibly null.
*/
@Override
public void onCancel(Object arguments) {
_eventSink = null;
}
  final Handler handler = new Handler();
final Runnable runnable = new Runnable() {
@Override
public void run() {
if(_eventSink != null) {
// 5. Emit events to Flutter
if (/*condition*/) {
// 5-1. Success
_eventSink.success("Hello");
} else {
// 5-2. Error
_eventSink.error("ERROR", "errorMessage", null);
}
}
handler.postDelayed(this, 10000);
}
};
handler.post(runnable);
}
  • Static methods need to match public static void registerWith(Registrar registrar), maybe...

iOS

@interface HelloPlugin : NSObject<FlutterPlugin, FlutterStreamHandler>
@end
// Implemented by the iOS part of a FlutterPlugin and FlutterStreamHandler.
@implementation HelloPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
// 2. Create EventChannel
FlutterEventChannel* channel = [FlutterEventChannel
eventChannelWithName:@"hello"
binaryMessenger:[registrar messenger]];
// 3. Plugin registration
HelloPlugin* instance = [[HelloPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:methodChannel];
}
// 1. Implements method of StreamHandler
/**
Sets up an event stream and begin emitting events.
 - Parameters:
- arguments: Arguments for the stream.
- events: A callback to asynchronously emit events.
- Returns: A FlutterError instance, if setup fails.
*/
- (FlutterError *)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {
// 4. Set up an event stream
_eventSink = eventSink;
return nil;
}
/**
Tears down an event stream.
 - Parameter arguments: Arguments for the stream.
- Returns: A FlutterError instance, if teardown fails.
*/
- (FlutterError *)onCancelWithArguments:(id)arguments {
_eventSink = nil;
return nil;
}
- (void)emit {
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
if(_eventSink != null) {
// 5. Emit events to Flutter
if (/*condition*/) {
// 5-1. Success
_eventSink("Hello");
} else {
// 5-2. Error
_eventSink([FlutterError errorWithCode:@"ERROR"
message:"errorMessage"
details:nil]);
}
}
}];
}
@end

Flutter

class Hello {
// 1. Create EventChannel
static const EventChannel _channel = const EventChannel('hello');
  Hello() {
// 2. Set listener of receive broadcast stream
_eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
  // 3. Handling event stream
void _onEvent(Object event) {
debugPrint(${event});
}
  // 4. Handling error stream
void _onError(Object error) {
debugPrint(${error});
}
}

How to handle 3rd-Party Apps in a plugin packages

General applications can call 3rd-Party Apps on MainActivity or AppDelegate/ViewController.

Below is an example of login processing using SDK of LINE application.

For Android, you need to call the LINE application with startActivityForResult and handle the authentication result returned by onActivityResult.

public void login() {
try{
// App-to-app login
Intent loginIntent = LineLoginApi.getLoginIntent(v.getContext(), CHANNEL_ID);
startActivityForResult(loginIntent, REQUEST_CODE);
    } catch(Exception e) {
Log.e("ERROR", e.toString());
}
}
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
    LineLoginResult result = LineLoginApi.getLoginResultFromIntent(data);
switch (result.getResponseCode()) {
case SUCCESS:
// Login successful
String accessToken = result.getLineCredential().getAccessToken().getAccessToken();
// ...
break;
      case CANCEL:    
// Login canceled by user
break;
    }
}

In case of iOS, it is necessary to process the result of the authentication process returned from the LINE application in AppDelegate.

@implementation AppDelegate
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options
{
return [[LineSDKLogin sharedInstance] handleOpenURL:url];
}
@end

PluginRegistry.Registrar or FlutterPluginRegistrar

A plugin packages does not have MainActivity or AppDelegate/ViewController.

So what shall we do?

It can be solved by using PluginRegistry.Registrar or FlutterPluginRegistrar.

Below is the excerpt from flutter_line_login.

class FlutterLineLoginPlugin(registrar: Registrar) : MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.ActivityResultListener {
    companion object {
        @JvmStatic
fun registerWith(registrar: Registrar): Unit {
FlutterLineLoginPlugin(registrar)
}
}
    init {
// 1. Create MethodChannel
val methodChannel = MethodChannel(registrar.messenger(), "net.granoeste/flutter_line_login")
// 2. Create EventChannel
val eventChannel = EventChannel(registrar.messenger(), "net.granoeste/flutter_line_login_result")
        // 3. Plugin registration
methodChannel.setMethodCallHandler(this)
eventChannel.setStreamHandler(this)
        // 4. Add instance of plugin to ActivityResultListener.
registrar.addActivityResultListener(this)
}
@interface FlutterLineLoginPlugin : NSObject<FlutterPlugin, LineSDKLoginDelegate, FlutterStreamHandler>
@end
@implementation FlutterLineLoginPlugin {
LineSDKAPI *apiClient;
}
+ (void)registerWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {
    // 1. Create MethodChannel
FlutterMethodChannel *methodChannel =
[FlutterMethodChannel methodChannelWithName:@"net.granoeste/flutter_line_login"
binaryMessenger:[registrar messenger]];
// 2. Create EventChannel
FlutterEventChannel *eventChannel =
[FlutterEventChannel eventChannelWithName:@"net.granoeste/flutter_line_login_result"
binaryMessenger:[registrar messenger]];
// 3. Plugin registration
FlutterLineLoginPlugin *instance = [[FlutterLineLoginPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:methodChannel];
[eventChannel setStreamHandler:instance];
    // 4. Add instance of plugin to Application Delegate.
[registrar addApplicationDelegate:instance];
}

I’m publishing the Flutter LINE login package using the previous techniques.

Please use it if you’d like.