Type Safe Custom Platform Specific Code to Communicate with Kotlin & Swift in Flutter
Leveraging Interoperability for Seamless Integration between Flutter, Kotlin, and Swift

Flutter is a popular open-source framework for building cross-platform mobile applications. However, there may be certain functionality that is not available in the Flutter framework, but is available in the platform-specific APIs or native libraries.
To access this functionality, you can use custom platform-specific code in Flutter. This allows the Flutter app to interact with device sensors, use platform-specific APIs and more. In this article, we will discuss how to write type-safe custom platform-specific code in Flutter and communicate with Kotlin and Swift.
Outline
- Intro to Platform Specific Code
- Architectural Overview
- Supported Data Type
- Type Safe Method Channel
- Write Platform Specific Code
- Conclusion
Intro to Platform Specific Code
In Flutter, custom platform-specific code refers to the implementation of functionality that is not available in the framework, but can be accessed by the app through platform-specific APIs or native libraries.
This allows the Flutter app to access native platform functionality that is not available in the framework, such as interacting with device sensors or using platform-specific APIs. By using meethod channel the Dart code is able to call a specific method in the platform-specific code and receive a response, enabling the two sides to exchange data and trigger actions.
Architectural Overview
In diagram below, the communication between the flutter application (client) and the iOS/Android platform (host) is done through the use of platform channels.

The architecture of a platform channel in Flutter typically includes the following components:
- Flutter app (client): This is the Dart code that runs in the Flutter framework and is responsible for creating the channel, sending messages, and receiving responses.
- Method Channel: This is the object that handles the communication between the Dart code and the platform-specific code. It defines the name of the channel and the methods that can be called.
- Platform-specific code (iOS Host & Android Host): This is place we put code written in languages such as Java, Swift, or Objective-C. It receives messages from the Dart code, performs the requested actions, and sends responses back.
Platform Channel Data Type Supported
The data type that is used on a platform channel can be any serializable object, such as a String, int, or a custom class. The serialization and deserialization of these values to and from messages happens automatically when you send and receive values

Type Safe Method Channel
By default flutter support MethodChannel
to communicate between the host and client, but method channel isn’t typesafe. Calling and receiving messages depends on the host and client declaring the same arguments and datatypes in order for messages to work and invoking the channels in the right way is typically error-prone.
The process of writing the interfaces on both Android and iOS hosts , that’s why the Flutter community has introduced Pigeon, a code generator tool to make communication between Flutter and the host platform type-safe & easier.
Write Platform Specific Code
In this case we have requriement to get device information from our application which are iOS and Android, the information that we want to retrive from specific platform such as
- Application ID (Bundle ID)
- App Version
- Device Name
- OS version
- Battery level
Step 1 Create Flutter Application
First we will create a simple flutter application to show information that we get from platform specific code. Create a project by using this command flutter create my_project_name
Step 2 Install Dependency
We need to add pigeon
into our pubspec.yaml
, since the pigeon is only used during development we have to declare it inside dev_dependencies
dev_dependencies:
flutter_test:
sdk: flutter
pigeon: ^7.1.4
Step 3 Defining AppDeviceHelper API
Pigeon operates in a straightforward manner, the API is defined in a Dart class outside the library folder. The API class is an abstract one with the @HostApi(). For this tutorial we will create api class inside pigeons directory
Project Structure
- lib
- pigeons/app_device_helper.dart
// pigeons/app_device_helper.dart
import 'package:pigeon/pigeon.dart';
class AppInfo {
final String appID;
final String appVersion;
AppInfo(this.appID, this.appVersion);
}
class DeviceInfo {
final String deviceName;
final String osVersion;
DeviceInfo(this.deviceName, this.osVersion);
}
@HostApi()
abstract class AppDeviceHelper {
AppInfo getAppInfo();
DeviceInfo getDeviceInfo();
int getBatteryLevel();
}
Step 4 Generate the Code
The next action involves allowing Pigeon to complete its task by producing the code from the pigeons/app_device_helper.dart
file. Open a terminal and enter the command.
flutter pub run pigeon \
--input pigeons/app_device_helper.dart \
--dart_out lib/app_device_helper.dart \
--java_package "com.example.medium_platform_channel" \
--java_out android/app/src/main/java/com/example/medium_platform_channel/PigeonAppDeviceHelper.java \
--experimental_swift_out ios/Runner/AppDeviceHelper.swift
We decided to use Swift which has experimental support for now rather than Objective-C, but if you want to use Objective-C you can remove the experimental_swift_out
and use these arguments
--objc_header_out ios/Runner/AppDeviceHelper.h \
--objc_source_out ios/Runner/AppDeviceHelper.m \
Command line argument explanation:
input
argument should be the file we defined the API in, anddart_out
should be in ourlib
folder, as it's the code we'll actually be using in our app.java_package
is full package name that can be find inapplicationId
fromandroid/src/build.gradle
java_out
is the path to the Java file that will be generated.experimental_swift_out
is the path to the Swift file that will be generated.objc_header_out
&objc_source_out
is path to the objective header (.h) and .m file
Step 5. Flutter Implementation
In flutter implementation we will create a simple page, a stateful page that will get information from method channel then display it into text
import 'package:flutter/material.dart';
import 'package:medium_platform_channel/app_device_helper.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
AppInfo appInfo = AppInfo(appID: "", appVersion: "");
DeviceInfo deviceInfo = DeviceInfo(deviceName: "", osVersion: "");
int batteryLevel = 0;
AppDeviceHelper deviceHelper = AppDeviceHelper();
@override
void initState() {
super.initState();
fetchData();
}
void fetchData() async {
final appInfoRetrieved = await deviceHelper.getAppInfo();
final deviceInfoRetrieved = await deviceHelper.getDeviceInfo();
final batteryLevelRetrieved = await deviceHelper.getBatteryLevel();
setState(() {
appInfo = appInfoRetrieved;
deviceInfo = deviceInfoRetrieved;
batteryLevel = batteryLevelRetrieved;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: const Text("Platform Channel")),
body: Center(
child: Column(
children: [
const Text(
"Platform Channel Example",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
const SizedBox(height: 16),
const Text(
"Application Info",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
Text("App ID: ${appInfo.appID}"),
Text("App Version: ${appInfo.appVersion}"),
const SizedBox(height: 8),
const Text(
"Device Info",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
Text("Device Name: ${deviceInfo.deviceName}"),
Text("OS Version: ${deviceInfo.osVersion}"),
Text("Battery Level: ${batteryLevel}%"),
],
),
),
),
);
}
}
First we need to import generated class from pigeon import ‘package:medium_platform_channel/app_device_helper.dart’;
After that we create a state about application info & device info, we need to create async method since AppDeviceHelper
so that we can call Future<>
function inside that class and update our state
AppInfo appInfo = AppInfo(appID: "", appVersion: "");
DeviceInfo deviceInfo = DeviceInfo(deviceName: "", osVersion: "");
int batteryLevel = 0;
AppDeviceHelper deviceHelper = AppDeviceHelper();
@override
void initState() {
super.initState();
fetchData();
}
void fetchData() async {
final appInfoRetrieved = await deviceHelper.getAppInfo();
final deviceInfoRetrieved = await deviceHelper.getDeviceInfo();
final batteryLevelRetrieved = await deviceHelper.getBatteryLevel();
setState(() {
appInfo = appInfoRetrieved;
deviceInfo = deviceInfoRetrieved;
batteryLevel = batteryLevelRetrieved;
});
}
Step 6. iOS Native Implementation
We now have the generated code form AppDeviceHelper interface / protocol, what we need to do is to create the implementations
// ios/Runner/AppDeviceHelperPlugin.swift
import Foundation
import Flutter
public class AppDeviceHelperPlugin: NSObject, AppDeviceHelper {
func getAppInfo() throws -> AppInfo {
let appID: String = Bundle.main.bundleIdentifier ?? ""
let appVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
return AppInfo(appID: appID, appVersion: appVersion)
}
func getDeviceInfo() throws -> DeviceInfo {
let deviceName: String = UIDevice.current.name
let osVersion: String = UIDevice.current.systemVersion
return DeviceInfo(deviceName: deviceName, osVersion: osVersion)
}
func getBatteryLevel() throws -> Int32 {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
return Int32(device.batteryLevel * 100)
}
public static func register(messenger: FlutterBinaryMessenger) {
let api: AppDeviceHelper & NSObjectProtocol = AppDeviceHelperPlugin()
AppDeviceHelperSetup.setUp(binaryMessenger: messenger, api: api)
}
}
The implementation is quite simple, we can get information about application info
by reading data from bundle
, then we get device info
we can use UIDevice.current
class, we also need to create a regiter
method to register the plugin in AppDelegate
file
// ios/Runner/AppDelegate.swift
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// register plugin
AppDeviceHelperPlugin.register(messenger: window.rootViewController as! FlutterBinaryMessenger)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Step 7. Android Implementation
We have create an iOS implementation since we will support Android OS we need to create the implementation for android side.
We create a file AppDeviceHelperPlugin
that implement interface from PigeonAppDeviceHelper.AppDeviceHelper
in this class we will create an implementation to get information about device info and also application info
package com.example.medium_platform_channel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
class AppDeviceHelperPlugin(var context: Context) : PigeonAppDeviceHelper.AppDeviceHelper {
override fun getAppInfo(): PigeonAppDeviceHelper.AppInfo {
val appID = BuildConfig.APPLICATION_ID
val appVersion = BuildConfig.VERSION_CODE
return PigeonAppDeviceHelper.AppInfo.Builder().setAppID(appID).setAppVersion(appVersion.toString()).build()
}
override fun getDeviceInfo(): PigeonAppDeviceHelper.DeviceInfo {
val deviceName = Build.MODEL
val os = Build.VERSION.RELEASE
return PigeonAppDeviceHelper.DeviceInfo.Builder().setDeviceName(deviceName).setOsVersion(os).build()
}
override fun getBatteryLevel(): Long {
val batteryLevel: Int
val intent = ContextWrapper(context).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
return batteryLevel.toLong()
}
}
Next, we need to register AppDeviceHelperPlugin
into our MainActivity, so that flutter application can communicate with AppDeviceHelperPlugin
to get information
package com.example.medium_platform_channel
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// setup AppDeviceHelperPlugin
PigeonAppDeviceHelper.AppDeviceHelper.setup(flutterEngine.dartExecutor.binaryMessenger, AppDeviceHelperPlugin(context))
}
}
Step 7. Running the App
This is the last step for us to run our application and see the result, the application should be able to get information about app infor & device info based on platform that user use
iOS Device

Android Device

Conclusion
In this article, we have explored the concept of platform-specific code in Flutter and how it can be used to interact with native platform APIs or libraries. We have also discussed the architecture of platform channels and how it enables communication between the Flutter app and the iOS/Android platform. To make the communication type-safe and easier, we introduced the Pigeon code generator tool and showed how to use it to generate platform-specific code from a Dart API definition.
By using platform-specific code in Flutter, developers can access functionality that is not available in the framework and provide seamless integration between the Flutter app, Kotlin, and Swift. The use of Pigeon makes the communication process more streamlined, reducing the risk of errors and improving the overall quality of the application
You can download full source code in my repository below