Developing a Flutter Native Plugin —A Real-world Scenario
Writing native code in Xcode and Android Studio to communicate with Flutter
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
- Create Flutter Plugin
- Separate plugin behaviors from platform implementations (Federated Packages)
- Send a message to native platform.
- Receive a response from the native platform.
- Streaming events from native to Flutter
- Call dart methods from native
- Demo a runnable example app for SDK
- Write iOS native code in Xcode that communicate with dart code
- 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.

- Flutter app can send messages to the Host, host receives the message and calls platform-specific APIs — In native language.
- 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.

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:
- Communicate properly with the team to understand what they meant
- 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 Stream
s.
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)
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:

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 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.