flutter_platform_channels
Flutter - Platform Channels
Section titled “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.
Architecture Overview
Section titled “Architecture Overview”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.
Types of Platform Channels
Section titled “Types of Platform Channels”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 thanMethodChannel).
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.
1. Dart (Client) Side
Section titled “1. Dart (Client) Side”In your Flutter widget’s state or service class:
import 'package:flutter/material.dart';import 'package:flutter/services.dart'; // Required for Platform Channelsimport '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()));2. Android (Host) Side - Kotlin
Section titled “2. Android (Host) Side - Kotlin”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.NonNullimport io.flutter.embedding.android.FlutterActivityimport io.flutter.embedding.engine.FlutterEngineimport io.flutter.plugin.common.MethodChannel
// Required imports for battery levelimport android.content.Contextimport android.content.ContextWrapperimport android.content.Intentimport android.content.IntentFilterimport android.os.BatteryManagerimport android.os.Build.VERSIONimport 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 }}3. iOS (Host) Side - Swift
Section titled “3. iOS (Host) Side - Swift”Open the iOS part of your project (ios/ folder) in Xcode (ios/Runner.xcworkspace) and edit ios/Runner/AppDelegate.swift:
import UIKitimport 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)) } }}Important Notes:
Section titled “Important Notes:”- Error Handling: Always handle potential
PlatformExceptionin Dart and send appropriate errors (result.error(...)orFlutterError) 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.
When to Use Platform Channels
Section titled “When to Use Platform Channels”- 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.