Invoking Native Functions from Flutter

Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, offers great flexibility and performance. However, there are situations where you might need to tap into platform-specific capabilities or leverage existing native code for performance-critical tasks. This is where invoking native functions from Flutter comes into play. This process allows you to seamlessly bridge the gap between your Flutter app and native code, enhancing your app’s capabilities and performance.

What is Platform Channel in Flutter?

Platform Channels are the mechanism through which Flutter communicates with the host platform’s native code (written in languages like Swift/Objective-C for iOS and Java/Kotlin for Android). A Platform Channel is a bidirectional communication tunnel allowing you to call native code from Flutter and vice versa. The core idea revolves around a string-based channel name agreed upon by both the Flutter and native sides.

Why Invoke Native Functions from Flutter?

  • Access Native APIs: Use platform-specific APIs and features not available in Flutter directly (e.g., access low-level hardware features, specific device functionalities).
  • Leverage Existing Code: Integrate legacy or specialized codebases written in native languages without rewriting them in Dart.
  • Performance Optimization: Execute computationally intensive tasks using native code, which often performs better than Dart for certain types of operations.
  • Custom Native UI Elements: Embed custom-designed native UI elements seamlessly into your Flutter app.

How to Invoke Native Functions from Flutter

To invoke native functions from Flutter, you’ll need to use Platform Channels. Here’s a step-by-step guide on how to do it:

Step 1: Define a Method Channel in Flutter

In your Dart code, create a MethodChannel instance. This channel acts as a bridge between your Flutter code and the native platform code.


import 'package:flutter/services.dart';

const platform = MethodChannel('your.unique.channel.name');

Make sure the channel name ('your.unique.channel.name') is unique and consistent across your Flutter and native code. The best practice is to use reverse domain notation to prevent naming collisions (e.g., 'com.example.app/native_utils').

Step 2: Invoke a Method on the Platform Channel

Use the invokeMethod method to call a specific function defined in the native code. This function can accept arguments and return a result.


Future getPlatformVersion() async {
  String version;
  try {
    final result = await platform.invokeMethod('getPlatformVersion');
    version = 'Platform version: $result';
  } on PlatformException catch (e) {
    version = "Failed to get platform version: '${e.message}'.";
  }
  return version;
}

Here:

  • 'getPlatformVersion' is the name of the native method you want to invoke.
  • invokeMethod returns a Future that completes when the native code returns a result or throws an error.
  • Errors on the native side result in a PlatformException on the Flutter side, allowing for error handling.

Step 3: Implement Native Code on Android (Kotlin/Java)

On Android, modify your MainActivity.kt (or MainActivity.java) to handle method calls on the defined channel.


import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

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

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            if (call.method == "getPlatformVersion") {
                result.success(android.os.Build.VERSION.RELEASE)
            } else {
                result.notImplemented()
            }
        }
    }
}

Explanation:

  • We retrieve the MethodChannel with the specified channel name.
  • setMethodCallHandler handles method calls from Flutter.
  • When the method 'getPlatformVersion' is called, it returns the Android version.
  • If an unknown method is called, result.notImplemented() is invoked.

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

On iOS, modify your AppDelegate.swift (or AppDelegate.m) to handle method calls on the defined channel.


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 methodChannel = FlutterMethodChannel(name: "your.unique.channel.name",
                                              binaryMessenger: controller.binaryMessenger)
    methodChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "getPlatformVersion" {
        result(UIDevice.current.systemVersion)
      } else {
        result(FlutterMethodNotImplemented)
      }
    }
    
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Explanation:

  • The method channel is initialized with the specified name.
  • setMethodCallHandler is used to handle method calls from Flutter.
  • When the method 'getPlatformVersion' is called, it returns the iOS version.
  • For unknown method calls, result(FlutterMethodNotImplemented) is called.

Step 5: Call the Method from Flutter UI

Now you can call the method from your Flutter UI, using the getPlatformVersion function we defined in Step 2:


import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  String _platformVersion = 'Unknown platform version';

  @override
  void initState() {
    super.initState();
    getPlatformVersion().then((value) {
      setState(() {
        _platformVersion = value;
      });
    });
  }

  Future getPlatformVersion() async {
    const platform = MethodChannel('your.unique.channel.name');
    String version;
    try {
      final result = await platform.invokeMethod('getPlatformVersion');
      version = 'Platform version: $result';
    } on PlatformException catch (e) {
      version = "Failed to get platform version: '${e.message}'.";
    }
    return version;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Native Function Call Example'),
        ),
        body: Center(
          child: Text(_platformVersion),
        ),
      ),
    );
  }
}

The example showcases a basic UI that fetches and displays the platform version by calling the native function. Make sure the same channel name is used throughout the application.

Example: Passing Arguments to Native Functions

Let’s see an example where we pass arguments to the native functions and get a calculated result back.

Dart (Flutter)


Future addNumbers(int a, int b) async {
  try {
    final result = await platform.invokeMethod('add', {'a': a, 'b': b});
    return result as int;
  } on PlatformException catch (e) {
    print("Failed to add numbers: '${e.message}'.");
    return -1; // Error code
  }
}

Kotlin (Android)


MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
    call, result ->
    if (call.method == "add") {
        val a = call.argument("a")
        val b = call.argument("b")

        if (a != null && b != null) {
            result.success(a + b)
        } else {
            result.error("ARGUMENT_ERROR", "Missing arguments", null)
        }
    } else {
        result.notImplemented()
    }
}

Swift (iOS)


methodChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
    if call.method == "add" {
        guard let args = call.arguments as? [String: Any],
              let a = args["a"] as? Int,
              let b = args["b"] as? Int else {
            result(FlutterError(code: "ARGUMENT_ERROR", message: "Missing arguments", details: nil))
            return
        }
        result(a + b)
    } else {
        result(FlutterMethodNotImplemented)
    }
}

Call this addNumbers function and use it as required in your Flutter application’s logic and UI.

Conclusion

Invoking native functions from Flutter allows you to unlock platform-specific functionalities, leverage existing codebases, and optimize performance-critical tasks. By using Platform Channels, Flutter applications can communicate bidirectionally with native code written in Swift/Objective-C for iOS and Java/Kotlin for Android, greatly enhancing the capabilities and flexibility of your app. Understand how to correctly use Platform Channels for bridging your Dart code and native functionality in the most efficient and reliable manner.