Working with Platform Channels to Communicate with Native Code in Flutter

Flutter excels at building cross-platform applications with a single codebase. However, there are times when you need to access platform-specific features or leverage existing native code. Flutter provides a mechanism called Platform Channels to facilitate communication between your Flutter code and the native platform code (e.g., Kotlin/Java for Android, Swift/Objective-C for iOS).

What are Platform Channels?

Platform channels act as a bridge that allows Flutter code to send and receive messages to and from the native platform’s code. This is particularly useful for:

  • Accessing device-specific APIs (e.g., Bluetooth, sensors, etc.).
  • Reusing existing native libraries.
  • Implementing performance-critical code in native languages.

Why Use Platform Channels?

  • Allows access to platform-specific functionality not available directly in Flutter.
  • Enables reuse of existing native codebases.
  • Provides a way to improve performance for specific tasks.

How to Implement Platform Channels in Flutter

The process involves defining a channel in Flutter and implementing the corresponding message handlers on the native side.

Step 1: Define the Platform Channel in Flutter

In your Flutter code, define a MethodChannel:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State {
  static const platform = const 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 at $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('Platform Channel Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_batteryLevel),
            ElevatedButton(
              onPressed: _getBatteryLevel,
              child: Text('Get Battery Level'),
            ),
          ],
        ),
      ),
    );
  }
}

In this example:

  • A MethodChannel named com.example.app/battery is created. This name is unique and will be used on the native side to identify the channel.
  • The getBatteryLevel method is invoked on the channel.
  • The result from the native side is displayed in the UI.

Step 2: Implement the Native Side (Android)

For Android, you implement the message handler in Kotlin or Java:

import androidx.annotation.NonNull
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
import androidx.core.content.ContextCompat.getSystemService

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.example.app/battery"

    override fun configureFlutterEngine(@NonNull 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 batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        val batteryLevel: Int = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        return batteryLevel
    }
}

Key aspects:

  • A MethodChannel with the same name as the one in Flutter is created.
  • A MethodCallHandler is set to handle incoming method calls.
  • When the method getBatteryLevel is called, the native code retrieves the battery level and returns it as a result.

Step 3: Implement the Native Side (iOS)

For iOS, you implement the message handler in Swift or Objective-C:

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
      }

      self?.receiveBatteryLevel(result: result)
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func receiveBatteryLevel(result: FlutterResult) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    if device.batteryState == UIDevice.BatteryState.unknown {
      result(FlutterError(code: "UNAVAILABLE",
                          message: "Battery info unavailable",
                          details: nil))
    } else {
      result(Int(device.batteryLevel * 100))
    }
  }
}

Key points:

  • The same MethodChannel name as defined in Flutter is used.
  • A handler is set for method calls to respond when getBatteryLevel is called.
  • The native code fetches the battery level and passes it back as a result.

Best Practices for Using Platform Channels

  • Minimize Data Transfer: Pass only necessary data between Flutter and native code to reduce overhead.
  • Handle Errors Gracefully: Ensure both Flutter and native sides handle exceptions and errors properly.
  • Use Consistent Channel Names: Keep the channel names consistent between Flutter and native platforms to avoid confusion.
  • Asynchronous Operations: Use asynchronous operations to prevent blocking the UI thread.
  • Data Serialization: Use efficient data serialization methods to pass complex data structures.

Conclusion

Platform channels are a powerful tool in Flutter that allows seamless communication with native code. Whether you need to access device-specific features or reuse existing native libraries, platform channels provide a reliable way to extend the capabilities of your Flutter applications. By following best practices and understanding the communication flow, you can effectively integrate native functionalities into your cross-platform projects, thus enhancing their features and performance.