Type Safe Custom Platform Specific Code to Communicate with Kotlin & Swift in Flutter

Leveraging Interoperability for Seamless Integration between Flutter, Kotlin, and Swift

Dwi Randy Herdinanto
8 min readFeb 1, 2023

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:

  1. 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.
  2. 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.
  3. 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, and
  • dart_out should be in our lib 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 from android/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

--

--

Dwi Randy Herdinanto

A software developer that enthusiastic about mobile applications