Dart Tutorial

Flutter Lesson 65 of 83 5 min read

Platform Channels in Flutter: Calling Native Code

Call Kotlin and Swift code from Flutter with MethodChannel: send arguments, return results, handle errors and receive events.

On this page

Plugins cover nearly everything. Now and then you need a native feature that no plugin provides, or you must use a company’s native SDK. A platform channel lets your Dart code send a message to Kotlin on Android or Swift on iOS, and get an answer back.

How it works #

Dart                                    Native (Kotlin / Swift)
 │  invokeMethod('getBatteryLevel')  →   │
 │                                       │  reads the battery from the OS
 │  ←  result: 82                        │

Both sides agree on a channel name and on method names. Calls are asynchronous: Dart gets a Future.

The Dart side #

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

class BatteryService {
  static const _channel = MethodChannel('com.example.myapp/battery');

  Future<int?> batteryLevel() async {
    try {
      return await _channel.invokeMethod<int>('getBatteryLevel');
    } on PlatformException catch (e) {
      debugPrint('Native error: ${e.code} ${e.message}');
      return null;
    } on MissingPluginException {
      return null; // not implemented on this platform
    }
  }
}

class BatteryPage extends StatefulWidget {
  const BatteryPage({super.key});

  @override
  State<BatteryPage> createState() => _BatteryPageState();
}

class _BatteryPageState extends State<BatteryPage> {
  final _service = BatteryService();
  String _text = 'Tap to check';

  Future<void> _check() async {
    final level = await _service.batteryLevel();
    if (!mounted) return;
    setState(() => _text = level == null ? 'Not available' : 'Battery: $level%');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: FilledButton(onPressed: _check, child: Text(_text))),
    );
  }
}

Use a unique channel name, by convention your app id followed by a topic.

The Android side (Kotlin) #

In android/app/src/main/kotlin/.../MainActivity.kt:

package com.example.myapp

import android.content.Context
import android.os.BatteryManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

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

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getBatteryLevel" -> {
                        val manager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                        val level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
                        if (level >= 0) result.success(level)
                        else result.error("UNAVAILABLE", "Battery level not available", null)
                    }
                    else -> result.notImplemented()
                }
            }
    }
}

The iOS side (Swift) #

In ios/Runner/AppDelegate.swift:

import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(
      name: "com.example.myapp/battery",
      binaryMessenger: controller.binaryMessenger
    )

    channel.setMethodCallHandler { call, result in
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      UIDevice.current.isBatteryMonitoringEnabled = true
      let level = UIDevice.current.batteryLevel
      if level < 0 {
        result(FlutterError(code: "UNAVAILABLE", message: "Battery level not available", details: nil))
      } else {
        result(Int(level * 100))
      }
    }

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

The generated native files differ a little between Flutter versions. Keep whatever is already in your file and add the channel code to it. After editing native code, stop the app and run it again.

Sending arguments #

final sum = await _channel.invokeMethod<int>('add', {'a': 3, 'b': 4});
"add" -> {
    val a = call.argument<Int>("a") ?: 0
    val b = call.argument<Int>("b") ?: 0
    result.success(a + b)
}

Types that can cross #

DartKotlinSwift
nullnullnil
boolBooleanBool
intInt or LongInt
doubleDoubleDouble
StringStringString
Uint8ListByteArrayFlutterStandardTypedData
ListListArray
MapHashMapDictionary

Your own classes cannot cross directly. Convert them to a map.

The three results #

Every native handler must answer exactly once:

Native callDart sees
result.success(value)The future completes with the value
result.error(code, message, details)A PlatformException
result.notImplemented()A MissingPluginException

Never leave a call unanswered. The Dart future would wait for ever.

EventChannel: a stream from native #

MethodChannel is request and response. For continuous data, such as sensor readings or Bluetooth events, use an EventChannel, which appears in Dart as a stream.

const _events = EventChannel('com.example.myapp/charging');

Stream<bool> get chargingChanges =>
    _events.receiveBroadcastStream().map((event) => event as bool);

The native side implements a stream handler that sends events while someone is listening.

Type-safe channels with Pigeon #

String method names and untyped maps are easy to get wrong, and mistakes appear only at run time. The pigeon package, from the Flutter team, generates matching, type-checked Dart, Kotlin and Swift code from one Dart file that describes your API. Use it for anything beyond one or two methods.

Other ways to reach native code #

ToolFor
Platform channelsCalling platform APIs written in Kotlin, Swift and so on
dart:ffiCalling C libraries directly, with no message passing
jnigen, ffigenGenerating bindings to Java and Kotlin, or C and Objective-C libraries
Platform viewsEmbedding a native view, such as a map or web view, inside the Flutter UI
dart:js_interopCalling JavaScript on the web

Keep it behind an interface #

Put channel code in one class, as BatteryService does. The rest of the app depends on that class, not on the channel, so tests can substitute a fake and platforms without an implementation can return a sensible default.

Before you write a channel #

Search pub.dev first. A maintained plugin already handles permissions, lifecycle and the edge cases of each OS version. Write your own channel when no plugin exists or when you must integrate a specific native SDK. If the result is general, consider publishing it as a plugin with flutter create --template=plugin.

Try it yourself #

Add a method channel with a getDeviceName method. Implement it on Android (using android.os.Build.MODEL) and on iOS (using UIDevice.current.name), and show the result in your app. Make the Dart side return “Unknown device” on platforms where it is not implemented.

Practise in the playground Updated by Santosh Adhikari