Creating Custom Flutter Plugin

Standard

Introduction

Flutter is a mobile app development framework based on Dart language.  Theoretically apps written with Flutter/Dart only can be deployed on native Android and iOS platforms, as long as the required packages/plugins exist for both.  For common apps that do not require special features, as is often the case, Flutter provides many pre-built packages and plugins. If special features are required, then native implement for both Android and iOS must be developed.  In this blog post, I will cover how o create a plugin that for Mapbox navigation.  To follow alone, you should first install Android Studio, Xcode, Flutter and Visual Studio Code.

 

Creating a Starter Plugin

To create a custom Flutter plugin named flutter_dashphone_navigation, do the following:

$ flutter create --org ai.e_motion --template=plugin flutter_dashphone_navigation
$ cd flutter_dashphone_navigation/example
$ flutter build apk  #build android
   . . . 
$ flutter build ios --no-codesign # build ios
   . . .

Then open the Android project in example/Android using Android Studio, build it and run it.
Then do the same for the iOS project in example/ios using Xcode.  Running under Xcode may require signing; and you may get an error indicating that the ‘App.framework’ was built for iOS.  To solve this, simply delete example/ios/Flutter/App.framework, and build and run the app in Xcode again.  For Android the below screen shows the output.

Note the message in the middle of the screen?  On Android it says “Running on Android 9”; on iOS it says “Running on iOS 13.4”.  We will use it a starting point to trace back how Flutter plugin works.

 

First Thing First

In Flutter, the main program starts in example/lib/main.dart:

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_dashphone_navigation/flutter_dashphone_navigation.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future initPlatformState() async {
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      platformVersion = await FlutterDashphoneNavigation.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'),
        ),
      ),
    );
  }
}

In main.dart the we see the main function is calling the MyApp widget. All Flutter widgets have a build() function, which we can see have a Scaffold with a AppBar and a text in the center of the body.  The text is the “Running on Android 9” and “Running on iOS 13.4” displaying on the phone screen.  We can see that “Android 9” and “iOS 13.4” are from _platformVersion, which is extracted from the FlutterDashphoneNavigation class, an auto-generated wrapper class the interfaces Dart application to the native code.  The code can be found at lib/flutter_dashphone_navigation.dart.  Note that Flutter likes lower-case, underscore separated filenames.  Flutter integrates native platforms using MethodChannel, a messaging mechanism, as is depicted in the figure below.

Platform channels architecture

FlutterDashphoneNavigation simply encapsulates, or hides the complexity of MethodChannel communication.

import 'dart:async';

import 'package:flutter/services.dart';

class FlutterDashphoneNavigation {
  static const MethodChannel _channel =
      const MethodChannel('flutter_dashphone_navigation');

  static Future get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

From the code above, the call to platformVersion is converted to a ‘getPlatformVersion’ message.  But who is receiving the message on the other end?  We can find that from looking under the “native” part the flutter project.  Let’s first look into the Android side.

 

Native Implementation – Android

The android side we can find the file FlutterDashphoneNavigationPlugin.kt under android/src/main/kotlin/ai/e_motion/flutter_dashphone_navigation.

Look into the code, we can see the below function that handles the call methods.  It should be clear now how one can easily add more calls.  Now, it is recommended that the getPlatformVersion is kept and not deleted.

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
   if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
   } else {
      result.notImplemented()
   }
}

 

Native Implementation – iOS

Now let’s take a look at the native implementation for iOS.  We can find AppDelegate.swift under example/ios/Runner.  The function is shown below.

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

The line highlighted in red is from another auto-generated class, an Objective-C file, example\ios\Runner\GeneratedPluginRegistrant.m:

//
//  Generated file. Do not edit.
//

#import "GeneratedPluginRegistrant.h"

#if __has_include(<flutter_dashphone_navigation/FlutterDashphoneNavigationPlugin.h>)
#import <flutter_dashphone_navigation/FlutterDashphoneNavigationPlugin.h>
#else
@import flutter_dashphone_navigation;
#endif

@implementation GeneratedPluginRegistrant

+ (void)registerWithRegistry:(NSObject*)registry {
  [FlutterDashphoneNavigationPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterDashphoneNavigationPlugin"]];
}

@end

The key function calls registerWithRegistrar function of the FlutterDashphoneNavigationPlugin class.  The registerWithRegistrar() function is implemented by ios/Classes/FlutterDashphoneNavigationPlugin.m, which in turns calls the register() function in ios/Classes/SwiftFlutterDashphoneNavigationPlugin.swift:

import Flutter
import UIKit

public class SwiftFlutterDashphoneNavigationPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "flutter_dashphone_navigation", binaryMessenger: registrar.messenger())
    let instance = SwiftFlutterDashphoneNavigationPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    result("iOS " + UIDevice.current.systemVersion)
  }
}

When a MethodCall message is received, the handler() function performs the callback.  The default auto-generated code does not check for call method, and just return the iOS system version.  A better example would be:

public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
   if (call.method == "getPlatformVersion") {
      result("iOS " + UIDevice.current.systemVersion)
   }
}

With the native code dissected one should be able to extend the plugin code at-will, and use the example code to test it.  In future blog post I will discuss how to create a Mapbox turn-by-turn navigation plugin for Flutter, and publish it to PubDev.

 

References

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s