Accessing Platform-Specific APIs Directly via Platform Channels in Flutter

Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, provides a rich set of platform-agnostic APIs. However, there are times when you need to access platform-specific features or APIs that aren’t available in the Flutter framework. This is where Platform Channels come in.

What are Platform Channels in Flutter?

Platform Channels are a mechanism that allows Flutter code to communicate with the native code of the underlying platform (e.g., Android or iOS). This communication happens using message passing. You can invoke native APIs and receive results back in your Flutter application, enabling access to features like accessing device sensors, handling native notifications, or working with platform-specific SDKs.

Why Use Platform Channels?

  • Access to Native APIs: Utilize platform-specific features that aren’t exposed through Flutter’s framework.
  • Integration with Native Libraries: Incorporate native libraries or SDKs written in languages like Java/Kotlin (Android) or Swift/Objective-C (iOS).
  • Performance Optimization: Offload performance-intensive tasks to native code for better execution on specific platforms.

How to Implement Platform Channels in Flutter

Implementing Platform Channels involves writing code in both Flutter and the native platform (Android/iOS). Here are the key steps:

Step 1: Define a Channel Name

A unique channel name is used to identify the communication channel between Flutter and the native platform. This name should be consistent across both the Flutter and native code.

Step 2: Flutter Code

In the Flutter code, create a MethodChannel instance and invoke a method on it.


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

class PlatformChannelScreen extends StatefulWidget {
  @override
  _PlatformChannelScreenState createState() => _PlatformChannelScreenState();
}

class _PlatformChannelScreenState 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 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'),
            ),
          ],
        ),
      ),
    );
  }
}

Explanation:

  • MethodChannel Instance: A MethodChannel named 'com.example.app/battery' is created to communicate with the native code.
  • invokeMethod: The invokeMethod function is called with the method name 'getBatteryLevel', which triggers the corresponding native function.
  • Handling Results: The result from the native function is received asynchronously and the UI is updated using setState.
  • Error Handling: Any PlatformException is caught, and an error message is displayed.

Step 3: Android Native Code (Java/Kotlin)

In the Android platform code (MainActivity.kt or MainActivity.java), handle the method call on the channel and provide a result.


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.VERSION
import android.os.Build.VERSION_CODES

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 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
    }
}

Explanation:

  • MethodChannel Handler: A MethodChannel is created with the same name as in Flutter.
  • setMethodCallHandler: This handler listens for method calls from Flutter.
  • Method Handling: If the method is "getBatteryLevel", it calls the getBatteryLevel function.
  • Returning Results: The battery level is returned using result.success(), and errors are handled using result.error().
  • getBatteryLevel Function: This function retrieves the battery level using Android’s battery APIs.

Step 4: iOS Native Code (Swift/Objective-C)

Similarly, in the iOS platform code (AppDelegate.swift or AppDelegate.m), handle the method call on the channel and provide a result.


import UIKit
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 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))
    }
  }
}

Explanation:

  • FlutterMethodChannel Creation: A FlutterMethodChannel is created with the same channel name as used in Flutter.
  • setMethodCallHandler: This handler is set to listen for method calls from Flutter.
  • Method Handling: When the method is "getBatteryLevel", the receiveBatteryLevel function is called.
  • Battery Level Retrieval: The receiveBatteryLevel function uses iOS APIs to get the current battery level.
  • Returning Results: The result is returned to Flutter via the result closure. If there’s an error, a FlutterError is returned.

Advanced Usage and Considerations

  • Complex Data: Use StandardMessageCodec for complex data structures in the messages.
  • Asynchronous Operations: Properly handle asynchronous operations on both Flutter and native sides to prevent blocking the UI.
  • Error Handling: Implement robust error handling and propagate errors back to Flutter for better debugging and user feedback.
  • Security: Be cautious when exposing sensitive platform APIs and consider security implications.

Conclusion

Platform Channels are a powerful feature in Flutter that allow you to tap into the full potential of the underlying platform by accessing native APIs and libraries. By using Platform Channels effectively, you can enhance your Flutter applications with features and optimizations tailored to each platform. Understanding and correctly implementing Platform Channels can significantly extend the capabilities of your Flutter applications and provide a more comprehensive user experience.