Using MethodChannel for One-Way Communication with Native Code in Flutter

Flutter allows you to build cross-platform applications from a single codebase, leveraging its rich set of widgets and development tools. However, there are situations where you might need to access platform-specific features or native APIs that are not available through Flutter’s framework. In such cases, Flutter’s MethodChannel becomes an invaluable tool. This article explores how to use MethodChannel for one-way communication with native code in Flutter.

What is MethodChannel in Flutter?

MethodChannel is a mechanism in Flutter for communicating between Flutter’s Dart code and the native code of the platform (Android or iOS). It allows you to invoke methods on the native side from Dart code and, if needed, receive results back. In the context of one-way communication, you send data or commands to the native side without expecting a return value.

Why Use MethodChannel for One-Way Communication?

  • Access Native APIs: Utilize platform-specific APIs and functionalities not available in Flutter.
  • Background Tasks: Delegate resource-intensive tasks to native code to run in the background.
  • Hardware Interaction: Interface with device hardware features that require native implementation.
  • Existing Native Libraries: Integrate pre-existing native libraries and code into your Flutter app.

How to Implement One-Way Communication Using MethodChannel

Step 1: Setting Up the Flutter Side

First, you need to set up the Flutter side to invoke methods on the native side.


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

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State {
  static const platform = MethodChannel('your.channel.name');

  void _sendDataToNative(String message) async {
    try {
      await platform.invokeMethod('nativeMethodName', {'message': message});
      print('Data sent to native successfully.');
    } on PlatformException catch (e) {
      print("Failed to invoke method: '${e.message}'");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            _sendDataToNative('Hello from Flutter!');
          },
          child: const Text('Send Data to Native'),
        ),
      ),
    );
  }
}

Explanation:

  • Import Dependencies: Imports necessary Flutter packages, including flutter/services.dart for MethodChannel.
  • Create MethodChannel: Instantiates a MethodChannel with a unique channel name (your.channel.name). This name must match the channel name defined in your native code.
  • _sendDataToNative Method:
    • Takes a message as input, which is the data to be sent to the native side.
    • Calls platform.invokeMethod('nativeMethodName', {'message': message}) to invoke a method named nativeMethodName on the native side. The second argument is a map containing data to be passed to the native method.
    • Handles potential PlatformException if the method invocation fails.
  • UI Components:
    • A button is created, and when pressed, it calls the _sendDataToNative method to send a message to the native side.

Step 2: Implementing Native Code (Android – Kotlin Example)

Next, you need to implement the native code to handle the method invocation from Flutter.


import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.util.Log

class MainActivity: FlutterActivity() {
    private val CHANNEL = "your.channel.name"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            if (call.method == "nativeMethodName") {
                val message = call.argument("message")
                if (message != null) {
                    Log.d("NativeMethod", "Received message from Flutter: $message")
                    // Perform native task here
                    // If needed, you can use result.success(null) to send a result back (optional for one-way)
                    result.success(null)  // Indicate success even if not returning a value
                } else {
                    result.error("INVALID_ARGUMENT", "Message is null.", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }
}

Explanation:

  • Import Statements: Imports necessary Android and Flutter libraries.
  • Define CHANNEL: Declares a constant CHANNEL with the same channel name used in Flutter.
  • configureFlutterEngine Method:
    • Overrides the configureFlutterEngine method to set up the MethodChannel.
    • Creates a MethodChannel instance, associating it with the Dart executor and the defined CHANNEL.
    • Sets a method call handler using setMethodCallHandler, which listens for method calls from Flutter.
  • Method Call Handling:
    • Inside the handler, it checks if the method being called is nativeMethodName.
    • Retrieves the message argument passed from Flutter.
    • Logs the received message to the Android logcat.
    • Performs the required native task (in this case, logging the message).
    • Calls result.success(null) to indicate that the method call was successful, even though no value is returned (optional for one-way communication, but recommended).
    • If the message is null, it calls result.error to return an error to Flutter.
  • Handling Unknown Methods:
    • If the method name does not match nativeMethodName, it calls result.notImplemented() to indicate that the method is not implemented on the native side.

Step 3: Implementing Native Code (iOS – Swift Example)


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 channel = FlutterMethodChannel(name: "your.channel.name",
                                           binaryMessenger: controller.binaryMessenger)
    channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "nativeMethodName" {
        if let args = call.arguments as? Dictionary,
           let message = args["message"] {
          print("Received message from Flutter: (message)")
          // Perform native task here
          result(nil) // Indicate success even if not returning a value
        } else {
          result(FlutterError(code: "INVALID_ARGUMENT", message: "Message is invalid", details: nil))
        }
      } else {
        result(FlutterMethodNotImplemented)
      }
    }

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

Explanation:

  • Import Statements: Imports necessary Flutter and UIKit libraries.
  • Set Up FlutterMethodChannel:
    • Retrieves the FlutterViewController from the app’s window.
    • Creates a FlutterMethodChannel with the same channel name used in Flutter (“your.channel.name“) and the controller’s binary messenger.
  • Handle Method Call:
    • Sets a method call handler using setMethodCallHandler to listen for method calls from Flutter.
    • Checks if the method being called is nativeMethodName.
  • Process Arguments:
    • Retrieves arguments from the method call, expecting a dictionary with a “message” key.
    • Safely unwraps the message from the arguments.
  • Perform Native Task:
    • Prints the received message to the console (as a placeholder for a real native task).
    • Calls result(nil) to indicate successful execution of the method, even if no data is returned. This is crucial for Flutter to know the method call completed successfully.
  • Error Handling:
    • If the arguments are invalid (e.g., missing the “message” key), it returns a FlutterError with a code and message indicating the issue.
    • If the method name does not match nativeMethodName, it calls result(FlutterMethodNotImplemented) to indicate that the method is not implemented on the native side.

Complete Example – Flutter (Dart)


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

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State {
  static const platform = MethodChannel('your.channel.name');

  void _sendDataToNative(String message) async {
    try {
      await platform.invokeMethod('nativeMethodName', {'message': message});
      print('Data sent to native successfully.');
    } on PlatformException catch (e) {
      print("Failed to invoke method: '${e.message}'");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            _sendDataToNative('Hello from Flutter!');
          },
          child: const Text('Send Data to Native'),
        ),
      ),
    );
  }
}

Key Considerations

  • Error Handling: Always include error handling to catch potential exceptions and provide meaningful feedback to the user.
  • Channel Name: Ensure the channel name is unique and consistent between Flutter and native code.
  • Data Serialization: Be mindful of the data types that can be passed through MethodChannel. Simple types (strings, numbers, booleans) are straightforward, but complex objects might require serialization.

Conclusion

MethodChannel in Flutter is a powerful tool for enabling communication between Flutter’s Dart code and platform-specific native code. Whether it’s accessing native APIs, delegating tasks, or integrating existing libraries, MethodChannel facilitates a seamless bridge between the cross-platform world of Flutter and the native capabilities of Android and iOS. By following the outlined steps and examples, developers can effectively implement one-way communication to enhance their Flutter applications with native functionalities.