Better Programming

Advice for programmers.

Follow publication

Developing a Flutter Native Plugin —A Real-world Scenario

Hashem Abounajmi
Better Programming
Published in
11 min readMay 23, 2022
Photo by Artur Shamsutdinov on Unsplash

Have you ever wanted to talk to iOS / Android OS and to do specific action which is not available in Flutter? Maybe your team have separate internal packages for iOS and Android platforms which you now need to use it in Flutter. But how?

I came across a request from Naurt team to create a Flutter plugin for their iOS and Android SDKs. Their product is a location Optimization SDK; so they want their SDK also to be used in Flutter apps.

What you will learn

  1. Create Flutter Plugin
  2. Separate plugin behaviors from platform implementations (Federated Packages)
  3. Send a message to native platform.
  4. Receive a response from the native platform.
  5. Streaming events from native to Flutter
  6. Call dart methods from native
  7. Demo a runnable example app for SDK
  8. Write iOS native code in Xcode that communicate with dart code
  9. Write Android native code in Android Studio that communicates with dart code

Solution

Flutter uses Method Channel to call platform-specific APIs.

Method Channel is a flexible message passing system that allows bidirectional communication between the Flutter portion of the app and the non-Dart portion of the app.

Method Channel Architectural Overview
Method Channel Architectural Overview
  1. Flutter app can send messages to the Host, host receives the message and calls platform-specific APIs — In native language.
  2. The host can send a response back to the Flutter app

You can think of Method Channel as HTTP methods. We don’t use them to call a function on the server, rather than we bundle information, sent in a format that the server can understand, and reply back with a response.

So when the Flutter app gets built for Android it embeds Android SDK in the android app bundle and when it builds for iOS it embeds iOS SDK in the iOS app bundle.

SDK embedding in the platform binary

Step 1 — Create a new plugin project

In order to create a Flutter plugin, Open VSCode, then View > Command Palette > Flutter: New Project select Plugin Template and name it naurt. after project creation rename folder to naurt_platform_interface.Be note the difference between package and plugin. package contains only dart code. but plugin used to communicate with the underlying platform.

naurt_platform_interface contains only the abstract classes that defines what the plugin package requires from its platform-specific implementations. so methods are unimplemented.

PlatformInterface is a helper class to ensure subclasses of NaurtPlatform extend this class instead of implementing it. It helps if you later add any method to the interface, it doesn't break all platform specific implementations.

Delete naurt file, example folder and getPlatfromVersion() .

Target Folder Structure:

  • Naurt
    naurt_platform_interface
    naurt_ios
    naurt_android
    naurt_plugin

naurt_ios and naurt_android are platform specific implementations of naurt_platform_interface. naurt_plugin is references all of those packages, and in Flutter apps we set naurt_plugin as the dependency. So later time if a windows plugin become necessary, it needs to create another package that implements the interface specifically for windows.

Define Platform Interface methods and properties

Look at both iOS / Android SDK methods. they are almost identical, with a little diversion. First step is create a common interface that all platforms should use it. Also, the team mentioned that properties in the platform-specific SDKs are observable and user can listen to their changes. it means e.g. in iOS / Android if the client wants to monitor location update, they need to listen to lastNaurtPoint property.

Let’s convert one method and one property. the rest will be same.

iOS: Naurt.shared.initialise( apiKey: String, precision: Int)

Android: Naurt.initialise( apiKey: String, context: Context, precision: Int): CompletableFuture<NaurtTrackingStatus>

And to check if SDK properly initialized there is observable isInitialised property.

iOS: @Published isInitialised: Bool

Android:var isInitialised = false

by observing isInitialised in the platform-specific language (Swift, Kotlin), moments later, SDK notify you if it’s properly initialized or not. We are not looking for judge their implementation. we need to define a simple interface in Dart to accommodate our use case in Flutter app.

Proposed Dart Method: Future<bool> initialize({required String apiKey, required int precision})

as you see we have combined tow above method and property in one unified Dart method with bool return value to simplify our plugin usage in Flutter app.

The goal is to:

  1. Communicate properly with the team to understand what they meant
  2. propose a nice solution.

There are two properties that are interesting in how we map to dart:

/** Is Naurt's Locomotion running at the moment? */
@Published isRunning: Bool
/** Most recent naurt point for the current journey* nil if no data is available */
@Published naurtPoint: NaurtLocation?

Dart Methods

In Dart, we can use Stream or callBacks to observe changes. I prefer stream for Location change because I can manipulate stream data with, map, reduce, but with call back is not possible. callBack for Boolean is much handier than Streams.

You can view Platform Interface methods here:

Step 2: Create iOS implementation

Create another plugin project name it naurt_ios and save it in the naurt directory. Open pubspec.yaml and add naurt_platfrom_interface as a dependency:

naurt_platfrom_interface:
path: ../naurt_platfrom_interface/

Also, addpublish_to: 'none' on top of the file — as for this tutorial we are not publishing to pub.dev — else all the dependencies must be uploaded to pub.dev.

Rename naurt_ios_method_channel to naurt_ios and its class to NaurtIOS, delete other files in the lib folder. delete example and test folders also.

Create a missing iOS folder

Run the below command in terminal to recreate the plugin project with the iOS folder. We will write Swift codes in the iOS folder to handle messages from the Flutter app:

flutter create --template=plugin --org=com.naurt --platforms=ios .

Open pubspec.yaml file and set plugin class for iOS under flutter section:

plugin:
platforms:
ios:
dartPluginClass: NaurtIOS
pluginClass: NaurtIosPlugin

NaurtIOS is the name of the Dart class that has implemented the interface,

NaurtIosPlugin is the name of the iOS class that method channel transfer messages to. you can find the class on ios/classes path. Flutter auto-generates the Swift version with a name SwiftNaurtIosPlugin:

Open NaurtIOS file extend it from NaurtPlatform and override the methods:

We defined a method channel with name com.naurt.ios to send message to iOS.

registerWith will be called during startup by naurt_plugin package to register the dart class that has implemented the platform interface and communicate with native platform. We will discuss on that later.

In the original Naurt SDK readme, it is also mentioned the SDK should be a singleton. So singleton in dart is simple as that by adding named constructor with _.

In the initialize method, method channel calls sends a message name to initialize to the iOS side of the project with apiKey and precision parameters. We are phrasing “sending message”, because the iOS side can ignore this message and nothing happens.

Add Naurt iOS SDK dependency

Open iOS/naurt_ios.podspec and add Naurt iOS SDK dependency:

s.dependency 'naurt_cocoapod', '0.6.0'
s.xcconfig = { 'ENABLE_BITCODE' => 'NO', }
s.platform = :ios, '13.4'

We also disabled bitcode for the dependency because from the SDK documentation is mentioned it should be disabled.

if in your case, the dependency is not available on cocoapods, and you have it as framework, navigate to the platform implementation path like: naurt_ios/ios/Frameworks move the the framework in to the folder and update the podspec as follow:

s.vendored_frameworks = 'Frameworks/FaceTecSDK.framework'

then in termnial navigate to naurt_ios/example/iOS and type pod install to integrate the dependency with the project.

Then how do we handle method channel messages in iOS? Where we should write Swift codes?

We’ll write all Swift codes in SwiftNaurtIosPlugin.swift.

iOS Plugin Implementation in Xcode

Right-click on example/iOS and open in Xcode. Its time to write Swift code and handle messages from Flutter side.

Navigate to SwiftNaurtIosPlugin.swift. The path to the file is deep nested but you can find out by looking at the below image:

Import naurt_framework and build the project to verify everything works fine.

So how does SwiftNaurtIosPlugin receive messages from the method channel?

If you remember we set NaurtIosPlugin as iOS plugin class in naurt_ios pubspec.yaml.

In the register method, a method channel with name com.naurt.ios is created. You can think of binaryMessenger a tool that encodes and decode messages. In order to handle messages in the channel, we need to override:

func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult)
handling method call

Call native method from Dart

Inside the method we check if initialize method is called — then we extract the method arguments from the call and pass it to iOS SDK initialize method.

If we look at NaurtPlatform initialize method, it expects a bool return value.

In order to find out initialization result, we need to observe isInitialized Naurt iOS SDK property and pass the value to the handle(_ :FlutterMethodCall, result: @escaping FlutterResult) result parameter.

result is used to pass the return value to the caller. Now you understood how call a method in native and return a value to dart. Because Naurt iOS SDK internally use Combine framework, we need to keep list of subscriptions, otherwise we can’t observe changes.

Calling Dart method from native

If you remember we defined a callback property in the interface named:

ValueChanged<bool>? onRunning;

We use this callback to observe SDK running status. You saw in the native how we used invokeMethod on channel to call onRunning in Dart with status value.

In Dart we handle method call in same way. Here’s my Dart method call handling in NaurtIOS initializer:

NaurtIOS() {
methodChannel.setMethodCallHandler((call) async {
if (call.method == 'onRunning') {
onRunning?.call(call.arguments);
}
});
}

Streaming Events from native to Dart

Same as method channel, you need an event channel to open and use an event sink to put stream of events in channel. For each specific event, you need an specific event sink. In Flutter we need to listen to the location change.

First, we need to create an event channel:

static const EventChannel _eventChannel = EventChannel('com.naurt.ios');

Then we listen to location changes in this way and we map it to NaurtLocation:

@override
Stream<NaurtLocation> get onLocationChanged {
return eventChannel
.receiveBroadcastStream()
.where((location) => location != null)
.map((dynamic location) =>
NaurtLocation.fromMap(Map<String, dynamic>.from(location)));
}

And in iOS we create an event channel with the same identifier in register method:

let eventChannel = FlutterEventChannel(name: "com.naurt.ios", binaryMessenger: registrar.messenger())
eventChannel.setStreamHandler(instance)

We create a property named locationUpdateEventSink specific for sinking location update.

private var locationUpdateEventSink: FlutterEventSink?

Then we set the event sink when the event channel is opened and clear it when it's closed:

public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
locationUpdateEventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
locationUpdateEventSink = nil
return nil
}

By observing naurtPoint for updates, we pass updates to the event sink:

subscriptions.append(Naurt.shared.$naurtPoint.sink { [weak self ]   value in
self?.locationUpdateEventSink?(value?.encode())
})

encode is an extension to NaurtLocation object to convert it map.

We can only pass primitive data types in the method and event channel.

private extension NaurtLocation {
func encode() -> [String: Any]{
return ["latitude": latitude, "longitude":longitude, "timestamp": timestamp]
}
}

To verify plugin correctness, I have added an example to the naurt-ios plugin folder that just tests basic behaviors of the SDK:

View full iOS Plugin source code in Github

Step 3: Create Naurt Android Plugin Project

Create a new plugin project and name it naurt_android.

Delete pubspec.yaml file. We’ll recreate it now.

Run the below command to create android and example folder inside naurt_android directory.

flutter create --template=plugin --org=com.naurt --platforms=android .

pubspec.yaml is recreated with proper class and package name.

flutter:
plugin:
platforms:
android:
package: com.naurt.naurt_android
pluginClass: NaurtAndroidPlugin
dartPluginClass: NaurtAndroid

NaurtAndroidPlugin is a Kotlin file where we write android native code. Also, add naurt_platfrom_interface in the dependencies section:

naurt_platfrom_interface:
path: ../naurt_platfrom_interface/

Delete naurt_android_platform_interface.dart and naurt_android_method_channel.dart files we don’t need them.

Dart implementation of the interface is the same as iOS.

But it can be different based on the requirements. Flexibility is the benefit of segregation of implementations.

It means two different team can implement the same interface in any way they prefer.

Android Plugin Implementation in Android Studio

Open whole naurt_android folder in Android Studio — by opening Android Studio: File > Open > “path to naurt_android project"

Then install Flutter plugin for Android Studio: Android Studio > Preferences > Plugins > Flutter and install it. then restart Android Studio. It loads all the Flutter packages which native Android project requires. by doing so we are able to edit to Kotlin file of native plugin.

To enable code completion for Flutter you need to open Preferences again and search for Flutter in preferences and enable code completion:

Enable code completion for Flutter in Android Studio

Then select Packages mode from left side bar, select NaurtAndroidPlugin then on the right hand side, select Open for Editing in Android Studio

Finally we are able to edit the Android plugin Kotlin file with code completion.

Configuring Android Dependencies

Open build.gradle for naurt_android and add Naurt Android SDK dependencies, and add JitPack to the list of repository lists:

rootProject.allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}

Then tap on Sync Now on the top right corner of IDE to install the dependencies.

Add Android dependencies

Add required permissions to AndroidManifest.xml. now you can import the following in NaurtAndroidPlugin.kt and start writing Android implementation of the plugin in Android Studio.

import  com.naurt_kotlin_sdk.Naurt.INSTANCE as Naurt

I have written the whole android implementation. mostly same as iOS. you can look through the Android implementation here.

Step 4: Create Naurt Plugin Project

This is where we reference all iOS, Android, and other platform implementation of NaurtPlatform interface and create an example project that can run all platforms using appropriate implemented plugins.

Create a new plugin project and name it naurt_plugin.

Run below command to create an example folder inside naurt_plugin directory.

flutter create --template=plugin --org=com.naurt --platforms=iOS, android .

Open pubspec.yaml file and add naurt_platform_interface, naurt_ios and naurt_android in the dependencies section:

dependencies:
naurt_platfrom_interface:
path: ../naurt_platfrom_interface/
naurt_android:
path: ../naurt_android/
naurt_ios:
path: ../naurt_ios/

Then add naurt_ios as iOS platform implementation and respectively for android:

flutter:
plugin:
platforms:
ios:
default_package: naurt_ios
android:
default_package: naurt_android

Copy main.dart in naurt_ios or naurt_android example folder and reuse it innaurt_plugin example app.

Run the example app in the naurt_plugin app to see in action on Android and iOS devices.

From now on if you want to use the plugin in any Flutter app, just reference naurt_plugin in pubspec.yaml.

Because based on the configuration above, the plugin can understand which platform instance to lookup.

Conclusion

So that was some how heavy topic, but we could lift it 🏋️‍♀️.

You learned how to communicate with an underlying platform from Flutter side and vice versa — writing native code in Xcode and Android Studio to communicate with Flutter. Also, you practiced the federated plugin packages which is the recommended approach by the Flutter team.

You can view the full source code on Github.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app