Sending Data and Invoking Methods Between Flutter and Native Platform Code

Flutter’s ability to reuse code across multiple platforms (iOS, Android, web, desktop) is one of its greatest strengths. However, there are situations where you need to access platform-specific functionality that Flutter doesn’t natively provide. This is where platform channels come in. They enable you to send data and invoke methods between your Flutter code and native platform code (written in languages like Java/Kotlin for Android, and Objective-C/Swift for iOS).

Understanding Platform Channels

Platform channels are a mechanism for communication between the Dart code in your Flutter app and the native code on the host platform. They consist of two parts:

  • Method Channel (MethodChannel): Used for invoking methods and sending data back and forth between Dart and native code. This is the most common type of platform channel.
  • Basic Message Channel (BasicMessageChannel): Enables asynchronous communication of strings and semi-structured messages.
  • Event Channel (EventChannel): Used to stream continuous events from the native platform to your Flutter app. Useful for sensors, Bluetooth updates, etc.

This article primarily focuses on MethodChannel as it is the most frequently used for basic data transfer and method invocation.

Use Cases for Platform Channels

  • Accessing device sensors (e.g., gyroscope, accelerometer).
  • Interacting with platform-specific APIs (e.g., Bluetooth, camera features).
  • Using native libraries for image processing or audio manipulation.
  • Retrieving device-specific information (e.g., battery level, network connectivity).
  • Integrating with platform-specific SDKs.

How to Implement Platform Channels in Flutter

Let’s walk through a practical example of getting the device’s battery level using a platform channel.

Step 1: Define the Channel Name

Choose a unique name for your channel. This name will be used in both your Flutter code and your native code.

const platform = MethodChannel('com.example.battery_channel');

Step 2: Flutter (Dart) Code

Write the Dart code to invoke a method on the native platform and handle the response.

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Platform Channel Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Platform Channel Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  String _batteryLevel = 'Unknown battery level.';

  Future _getBatteryLevel() async {
    const platform = MethodChannel('com.example.battery_channel');
    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(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_batteryLevel),
            ElevatedButton(
              onPressed: _getBatteryLevel,
              child: const Text('Get Battery Level'),
            ),
          ],
        ),
      ),
    );
  }
}

Explanation:

  • A MethodChannel named com.example.battery_channel is created.
  • The _getBatteryLevel() function invokes the native method getBatteryLevel.
  • It uses try...catch to handle potential PlatformException errors that may occur if the native method fails.
  • The returned battery level (or error message) is displayed in a Text widget.

Step 3: Android (Kotlin) Code

Implement the method on the Android platform using Kotlin or Java. This code should be placed within your `MainActivity.kt` or `MainActivity.java` file.

package com.example.platformchannel

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.battery_channel"

    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:

  • The same channel name (com.example.battery_channel) is used.
  • MethodChannel is instantiated in the configureFlutterEngine method of the MainActivity.
  • The setMethodCallHandler listens for method calls on the channel.
  • When the getBatteryLevel method is called, the getBatteryLevel() function retrieves the battery level using Android’s battery APIs.
  • The result (battery level) is passed back to Flutter using result.success().
  • Errors are reported using result.error().
  • If an unknown method is called, result.notImplemented() is called.

Step 4: iOS (Swift) Code

Implement the corresponding method on the iOS platform using Swift. This code is typically added to `AppDelegate.swift` file.

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.battery_channel",
                                              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:

  • The same channel name (com.example.battery_channel) is used.
  • A FlutterMethodChannel is created with the channel name.
  • setMethodCallHandler listens for method calls on the channel.
  • When the getBatteryLevel method is called, receiveBatteryLevel() retrieves the battery level using iOS’s battery APIs.
  • The result (battery level) is passed back to Flutter using result().
  • Errors are reported using FlutterError.
  • If an unknown method is called, result(FlutterMethodNotImplemented) is called.

Sending Data to Native Code

You can also send data *from* your Flutter app *to* the native code when invoking a method. To do this, pass a Map or other serializable data structure as the `arguments` parameter in the `invokeMethod` call.

final result = await platform.invokeMethod('someNativeMethod', {'name': 'John Doe', 'age': 30});

In the native code, you can retrieve these arguments from the `MethodCall` object.

Android (Kotlin):

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
    if (call.method == "someNativeMethod") {
        val name = call.argument("name")
        val age = call.argument("age")

        // ... process the data ...

        result.success("Data received: Name = $name, Age = $age")
    } else {
        result.notImplemented()
    }
}

iOS (Swift):

batteryChannel.setMethodCallHandler({
  (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
    if call.method == "someNativeMethod" {
        let name = call.arguments as? String
        let age = call.arguments as? Int

        // ... process the data ...

        result("Data received: Name = (name ?? "Unknown"), Age = (age ?? 0)")

    } else {
        result(FlutterMethodNotImplemented)
    }
})

Best Practices for Using Platform Channels

  • Choose Descriptive Channel Names: Use clear and descriptive names for your platform channels. Consider using reverse domain notation to ensure uniqueness.
  • Handle Errors Gracefully: Implement error handling to catch potential exceptions and provide informative messages to the user. Use `try…catch` blocks in Dart and appropriate error handling in your native code.
  • Keep Channel Calls Minimal: Platform channel calls can have a performance overhead. Minimize the number of calls and the amount of data transferred where possible.
  • Asynchronous Communication: Platform channels operate asynchronously. Ensure your UI updates and logic are handled correctly after receiving data from the native side.
  • Use Constants: Define your method names as constants to avoid typos and maintain consistency.
  • Consider Code Generation: For more complex scenarios, consider using code generation tools to automatically generate the boilerplate code for platform channel communication.

Alternative Solutions

While platform channels are a fundamental tool for interoperability, consider these alternatives in specific situations:

  • Plugins: Check if a Flutter plugin already exists for the platform-specific functionality you need. Many popular features have existing, well-maintained plugins available on pub.dev.
  • FFI (Foreign Function Interface): For integrating with native libraries written in languages like C/C++, consider using Flutter’s FFI capabilities. This is typically used for performance-critical operations.

Conclusion

Platform channels are a powerful mechanism for bridging the gap between Flutter’s Dart code and native platform code. By using platform channels, you can access platform-specific APIs and functionalities that are not natively available in Flutter, unlocking the full potential of each platform while still enjoying the benefits of cross-platform development. Remember to follow best practices for efficient and robust integration.