Skip to content

flutter_platform_channels

While Flutter allows you to build beautiful UIs and application logic from a single Dart codebase, sometimes you need to access platform-specific APIs or services available only through native Android (Kotlin/Java) or iOS (Swift/Objective-C) code. Examples include using device sensors, accessing platform-specific hardware like the camera (beyond basic plugin support), integrating with existing native codebases, or utilizing platform-specific UI elements.

Flutter provides Platform Channels as a mechanism for communication between your Dart code and the native platform code.

Platform channels facilitate asynchronous message passing:

  • Client (Dart): Your Flutter application code sends messages (method calls with optional arguments) over a named channel.
  • Host (Platform Native): The native Android or iOS part of your app listens on the same named channel. When a message arrives, the host executes the requested native code and can optionally send a response back to the client.
  • Channel Name: A unique string identifies the channel (e.g., com.yourcompany.yourapp/channel_name).
  • Message Codec: Determines how messages (arguments and results) are serialized and deserialized between Dart and the native platform. The standard codec (StandardMethodCodec) supports basic types like null, booleans, numbers, Strings, byte arrays, lists, and maps.

Communication is asynchronous to prevent blocking the UI thread on either side.

  • MethodChannel: Used for invoking a single method (like a function call) and receiving a single result back. This is the most common type.
  • EventChannel: Used for streaming data from the native platform to Dart (e.g., sensor events, location updates, connectivity changes).
  • BasicMessageChannel: Used for basic, asynchronous message passing with a custom codec if needed (less common than MethodChannel).

Using MethodChannel (Example: Get Battery Level)

Section titled “Using MethodChannel (Example: Get Battery Level)”

Let’s create a simple example to retrieve the device’s current battery level using native APIs.

In your Flutter widget’s state or service class:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Required for Platform Channels
import 'dart:async';
class BatteryInfo extends StatefulWidget {
const BatteryInfo({Key? key}) : super(key: key);
@override
_BatteryInfoState createState() => _BatteryInfoState();
}
class _BatteryInfoState extends State<BatteryInfo> {
// Define the channel. Name must match the host side.
static const platform = MethodChannel('com.example.myapp/battery');
String _batteryLevel = 'Unknown battery level.';
// Method to invoke the platform-specific code
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
// Invoke 'getBatteryLevel' method on the channel.
// Result is expected to be an integer.
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
// Handle potential errors during the platform call
batteryLevel = "Failed to get battery level: '${e.message}'.";
} on MissingPluginException catch (e) {
// Handle case where the method is not implemented on the native side
batteryLevel = "Battery level method not implemented: '${e.message}'.";
}
// Update the UI
setState(() {
_batteryLevel = batteryLevel;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Platform Channel Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _getBatteryLevel,
child: const Text('Get Battery Level'),
),
const SizedBox(height: 16),
Text(_batteryLevel),
],
),
),
);
}
}
// --- Usage Example ---
// void main() => runApp(MaterialApp(home: BatteryInfo()));

Open the Android part of your project (android/ folder) in Android Studio or edit android/app/src/main/kotlin/.../MainActivity.kt:

package com.example.myapp // Replace with your package name
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
// Required imports for battery level
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() {
// Define the channel name (must match Dart side)
private val CHANNEL = "com.example.myapp/battery"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Set up the MethodChannel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
// Note: this method is invoked on the main thread.
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel) // Send result back to Dart
} else {
// Send an error back to Dart
result.error("UNAVAILABLE", "Battery level not available.", null)
}
} else {
// Indicate that the method call is not implemented
result.notImplemented()
}
}
}
// Private function to get battery level using Android SDK
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
}
}

Open the iOS part of your project (ios/ folder) in Xcode (ios/Runner.xcworkspace) and edit ios/Runner/AppDelegate.swift:

import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Get the FlutterViewController
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
// Define the channel name (must match Dart side)
let batteryChannel = FlutterMethodChannel(name: "com.example.myapp/battery",
binaryMessenger: controller.binaryMessenger)
// Set up the MethodCallHandler
batteryChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
// Note: this method is invoked on the UI thread.
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented) // Indicate method not implemented
return
}
// Call the private function to get battery level
self?.receiveBatteryLevel(result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Private function to get battery level using iOS SDK
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true // Enable monitoring
if device.batteryState == UIDevice.BatteryState.unknown {
// Send error back to Dart if state is unknown
result(FlutterError(code: "UNAVAILABLE",
message: "Battery level not available.",
details: nil))
} else {
// Battery level is float 0.0-1.0, convert to integer 0-100
result(Int(device.batteryLevel * 100))
}
}
}
  • Error Handling: Always handle potential PlatformException in Dart and send appropriate errors (result.error(...) or FlutterError) from the native side.
  • Threading: Native method handlers typically run on the platform’s main UI thread. Perform long-running tasks on background threads to avoid blocking the UI.
  • Type Safety: Ensure the data types sent and received match between Dart and native code. The standard codec handles basic types; for complex objects, serialize/deserialize to Maps/Lists.
  • Plugin Structure: For reusable platform-specific functionality, create a Flutter Plugin package instead of writing code directly in the app’s MainActivity/AppDelegate.
  • Accessing platform-specific hardware (sensors, Bluetooth, NFC, etc.).
  • Using platform-specific APIs not available through existing plugins (e.g., specific OS features, background execution modes).
  • Integrating with existing native code or SDKs.
  • Displaying native UI components within a Flutter app (using Platform Views, which build upon platform channels).

Platform channels are a powerful feature for extending Flutter’s capabilities, but often, existing plugins on pub.dev can provide the functionality you need without requiring direct native development.