Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, offers impressive cross-platform capabilities. However, there are times when you need to access platform-specific features and APIs that are not natively available in Flutter. This is where Platform Channels come in. Platform Channels provide a powerful mechanism to communicate between your Flutter code and the native code of the underlying platform (e.g., Android or iOS).
What are Platform Channels in Flutter?
Platform Channels are a way to invoke native code from your Flutter application, and vice versa. They allow you to send asynchronous method calls between the Flutter Dart code and the platform-specific code written in languages such as Kotlin or Java for Android, and Swift or Objective-C for iOS.
Why Use Platform Channels?
- Access Native APIs: Utilize platform-specific APIs and features unavailable in Flutter’s native widgets and services.
- Integrate Existing Native Code: Leverage existing native libraries and components.
- Performance Optimization: Offload performance-intensive tasks to native code for optimal execution on the target platform.
How to Implement Platform Channels in Flutter
Implementing Platform Channels involves the following steps:
Step 1: Define a Method Channel
In your Flutter code (Dart), create a MethodChannel
with a unique name. This name will be used to identify the channel on both the Flutter and native sides.
import 'package:flutter/services.dart';
const platform = MethodChannel('com.example.app/native_channel');
Here, com.example.app/native_channel
is the unique name for the channel.
Step 2: Invoke a Native Method
Use the invokeMethod
method on the MethodChannel
to call a method in the native code. Pass the method name and any required arguments.
Future getPlatformVersion() async {
String version;
try {
final result = await platform.invokeMethod('getPlatformVersion');
version = 'Platform version: $result';
} on PlatformException catch (e) {
version = "Failed to get platform version: '${e.message}'.";
}
return version;
}
In this example, getPlatformVersion
is the method called on the native side.
Step 3: Implement the Native Code
On the native side (Android or iOS), set up a MethodChannel
using the same name defined in your Flutter code. Implement a method handler to respond to the method calls from Flutter.
Android (Kotlin)
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/native_channel"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
}
}
iOS (Swift)
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let CHANNEL = "com.example.app/native_channel"
let methodChannel = FlutterMethodChannel(name: CHANNEL,
binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == "getPlatformVersion" else {
result(FlutterMethodNotImplemented)
return
}
result("iOS (UIDevice.current.systemVersion)")
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Step 4: Display the Result in Flutter
Back in Flutter, update your UI to display the result obtained from the native code.
import 'package:flutter/material.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();
getPlatformVersion().then((version) {
setState(() {
_platformVersion = version;
});
});
}
Future getPlatformVersion() async {
const platform = MethodChannel('com.example.app/native_channel');
String version;
try {
final result = await platform.invokeMethod('getPlatformVersion');
version = 'Platform version: $result';
} on PlatformException catch (e) {
version = "Failed to get platform version: '${e.message}'.";
}
return version;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Platform Channel Demo'),
),
body: Center(
child: Text(_platformVersion),
),
),
);
}
}
Example: Accessing Battery Level (Native API)
Another common use case is accessing the device’s battery level. The code below illustrates how to accomplish this via a Platform Channel.
Flutter (Dart)
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class BatteryLevelScreen extends StatefulWidget {
@override
_BatteryLevelScreenState createState() => _BatteryLevelScreenState();
}
class _BatteryLevelScreenState extends State {
static const platform = MethodChannel('com.example.app/battery');
String _batteryLevel = 'Unknown battery level.';
Future _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level: $result%.';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Battery Level'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_batteryLevel),
ElevatedButton(
onPressed: _getBatteryLevel,
child: Text('Get Battery Level'),
),
],
),
),
);
}
}
Android (Kotlin)
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryLevel: Int
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
} else {
val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
}
return batteryLevel
}
}
iOS (Swift)
import Flutter
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "com.example.app/battery",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
receiveBatteryLevel(result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: @escaping FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == .unknown {
result(FlutterError(code: "UNAVAILABLE",
message: "Battery info unavailable",
details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}
Best Practices for Platform Channels
- Keep Channels Narrow and Specific: Design channels for specific purposes rather than general-purpose communication.
- Handle Errors Gracefully: Always handle potential errors like
PlatformException
to prevent crashes. - Asynchronous Operations: Remember that calls are asynchronous. Use
async
andawait
for proper handling. - Document Channels Thoroughly: Proper documentation helps maintainability and collaboration.
- Test Thoroughly: Write unit and integration tests to ensure your channels work as expected across different platforms.
Conclusion
Platform Channels are an essential tool for Flutter developers who need to access platform-specific features and APIs. By understanding how to implement and use them effectively, you can greatly extend the capabilities of your Flutter applications and create truly seamless native integrations. When working with Platform Channels in Flutter, careful planning, error handling, and testing are crucial to ensure a robust and reliable implementation.