Skip to content

Flutter Native Channels — Platform-Specific Code and Method Channels

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Flutter Native Channels. We cover key concepts, practical examples, and best practices to help you master this topic.

Flutter native channels enable communication between Dart code and the host platform (Android/iOS) for accessing platform-specific APIs that are not available through existing Flutter plugins.

What Will You Learn

  • MethodChannel for invoking platform methods
  • Handling method calls on Android (Kotlin/Java)
  • Handling method calls on iOS (Swift/Objective-C)
  • EventChannel for continuous data streams
  • BasicMessageChannel for string messages
  • Error handling and platform response types
  • Creating a plugin from platform code

Why It Matters

Flutter plugins cover most common platform APIs, but some use cases require custom platform code: accessing hardware sensors, interacting with platform-specific SDKs, or embedding native views. MethodChannel provides a type-safe way to call platform code from Dart. Understanding native channels enables you to extend Flutter's capabilities beyond what plugins provide.

Real-World Use

The DodaTech Flutter app uses MethodChannel to access the device's battery level for a dashboard widget, to read NFC tags for asset tracking, and to integrate with the platform's biometric authentication (fingerprint/face ID). The EventChannel provides real-time sensor data for the weather feature.

Learning Path

flowchart LR
  A[Flutter Performance] --> B[Native Channels\nYou are here]
  B --> C[Flutter FFI]
  style B fill:#f90,color:#fff

MethodChannel Basics

Define a MethodChannel in Dart and invoke methods on it:

import 'package:flutter/services.dart';

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

  Future<int> getBatteryLevel() async {
    try {
      final result = await _channel.invokeMethod<int>('getBatteryLevel');
      return result ?? -1;
    } on PlatformException catch (e) {
      print('Battery error: ${e.message}');
      return -1;
    }
  }

  Future<bool> startCharging() async {
    try {
      final result = await _channel.invokeMethod<bool>('startCharging');
      return result ?? false;
    } on PlatformException catch (e) {
      print('Charging error: ${e.message}');
      return false;
    }
  }
}

The channel name (com.dodatech/battery) must match on both sides. Use invokeMethod<T> with the expected return type. Handle PlatformException for platform errors.

Android Platform Handler (Kotlin)

Handle method calls on the Android side in MainActivity.kt:

// android/app/src/main/kotlin/com/dodatech/myapp/MainActivity.kt
import android.os.BatteryManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.dodatech/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            CHANNEL
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getBatteryLevel" -> {
                    val batteryLevel = getBatteryLevel()
                    if (batteryLevel != -1) {
                        result.success(batteryLevel)
                    } else {
                        result.error("UNAVAILABLE", "Battery level not available", null)
                    }
                }
                "startCharging" -> {
                    // Simulate starting a charging process
                    result.success(true)
                }
                else -> result.notImplemented()
            }
        }
    }

    private fun getBatteryLevel(): Int {
        val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    }
}

setMethodCallHandler receives Dart invocations. Use result.success() for successful results, result.error() for errors, and result.notImplemented() for unknown methods.

iOS Platform Handler (Swift)

Handle method calls on the iOS side in AppDelegate.swift:

// ios/Runner/AppDelegate.swift
import UIKit
import Flutter

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

        batteryChannel.setMethodCallHandler { [weak self] (call, result) in
            switch call.method {
            case "getBatteryLevel":
                let batteryLevel = self?.getBatteryLevel() ?? -1
                if batteryLevel >= 0 {
                    result(batteryLevel)
                } else {
                    result(
                        FlutterError(
                            code: "UNAVAILABLE",
                            message: "Battery level not available",
                            details: nil
                        )
                    )
                }
            case "startCharging":
                result(true)
            default:
                result(FlutterMethodNotImplemented)
            }
        }

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

    private func getBatteryLevel() -> Int {
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        return Int(device.batteryLevel * 100)
    }
}

The iOS handler mirrors the Android handler structure. Use FlutterError for error responses. FlutterMethodNotImplemented handles unknown methods.

EventChannel

EventChannel streams continuous platform events to Dart:

// Dart side
class SensorService {
  static const _eventChannel = EventChannel('com.dodatech/sensor');

  Stream<double> get accelerometerStream {
    return _eventChannel.receiveBroadcastStream().map(
      (event) => (event as num).toDouble(),
    );
  }

  Stream<Map<String, dynamic>> get locationStream {
    return _eventChannel
        .receiveBroadcastStream('location')
        .map((event) => event as Map<String, dynamic>);
  }
}
// Android side
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    
    EventChannel(
        flutterEngine.dartExecutor.binaryMessenger,
        "com.dodatech/sensor"
    ).setStreamHandler(
        SensorStreamHandler()
    )
}

class SensorStreamHandler : EventChannel.StreamHandler {
    private var sensorManager: SensorManager? = null
    private var sensorEventListener: SensorEventListener? = null

    override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
        val context = // get context
        sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        sensorEventListener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent) {
                events.success(event.values[0])
            }
            override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
        }
        sensorManager?.registerListener(
            sensorEventListener,
            sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
            SensorManager.SENSOR_DELAY_NORMAL
        )
    }

    override fun onCancel(arguments: Any?) {
        sensorManager?.unregisterListener(sensorEventListener)
    }
}

EventChannel.onListen is called when Dart listens to the stream. onCancel is called when Dart cancels. Use eventSink.success() to emit data and eventSink.error() for errors.

BasicMessageChannel

BasicMessageChannel sends string or binary messages:

// Dart side
class PlatformMessages {
  static const _messageChannel = BasicMessageChannel<String>(
    'com.dodatech/messages',
    StringCodec(),
  );

  Future<String?> sendMessage(String message) async {
    try {
      return await _messageChannel.send(message);
    } on PlatformException catch (e) {
      print('Message error: ${e.message}');
      return null;
    }
  }

  Stream<String> get messageStream {
    return _messageChannel.receiveBroadcastStream().cast<String>();
  }
}

BasicMessageChannel is simpler than MethodChannel but supports only basic codecs: StringCodec, BinaryCodec, JSONMessageCodec, StandardMessageCodec.

Passing Complex Types

Pass maps and lists through MethodChannel:

// Dart: send a complex object
final result = await _channel.invokeMethod('processUser', {
  'name': 'Alice',
  'age': 30,
  'address': {
    'street': '123 Main St',
    'city': 'Springfield',
    'zipCode': '12345',
  },
  'tags': ['premium', 'active'],
  'score': 95.5,
});
// Kotlin: receive and process
"processUser" -> {
    val data = call.arguments as Map<String, Any?>
    val name = data["name"] as String
    val age = data["age"] as Int
    val address = data["address"] as Map<String, Any?>
    val tags = data["tags"] as List<String>
    val score = data["score"] as Double
    
    // Process user data
    result.success(mapOf(
        "processed" to true,
        "userName" to name.uppercase()
    ))
}

MethodChannel supports: null, bool, int, double, String, Uint8List, Int32List, Int64List, Float32List, Float64List, List, Map.

Error Handling

Handle errors consistently on both sides:

// Dart side with detailed error handling
Future<dynamic> safeInvoke(String method, [dynamic arguments]) async {
  try {
    return await _channel.invokeMethod(method, arguments);
  } on MissingPluginException {
    print('Method $method not implemented on platform');
    return null;
  } on PlatformException catch (e) {
    print('Platform error: ${e.code} - ${e.message}');
    return null;
  } catch (e) {
    print('Unexpected error: $e');
    return null;
  }
}
// Kotlin side with specific error codes
result.error(
    "PERMISSION_DENIED",
    "Camera permission is required",
    null
)
result.error(
    "NOT_AVAILABLE",
    "Feature not available on this device",
    null
)

Use consistent error codes between Dart and platform code. Handle MissingPluginException in Dart when the platform does not implement the method.

Creating a Plugin

Package platform code as a reusable plugin:

// lib/battery_plugin.dart
import 'package:flutter/services.dart';

class BatteryPlugin {
  static const _channel = MethodChannel('com.dodatech/battery');

  static Future<int> getBatteryLevel() async {
    return await _channel.invokeMethod<int>('getBatteryLevel') ?? -1;
  }
}

// In pubspec.yaml for plugin:
// flutter:
//   plugin:
//     platforms:
//       android:
//         package: com.dodatech.battery
//         pluginClass: BatteryPlugin
//       ios:
//         pluginClass: BatteryPlugin

Package plugins follow the same MethodChannel pattern but are published on pub.dev for reuse across projects.

Common Mistakes

  1. Channel name mismatch: The channel name string must be identical in Dart, Android, and iOS. A typo causes MissingPluginException.

  2. Blocking the platform thread: Platform method handlers run on the platform's main thread. Long operations must be dispatched to a background thread.

  3. Not handling MissingPluginException: If the platform code is not registered, Dart receives MissingPluginException. Always handle it gracefully.

  4. Sending unsupported data types: MethodChannel supports a limited set of types. Custom objects must be serialized to maps.

  5. Forgetting permission handling: Platform APIs often require permissions (camera, location, sensors). Handle permission denials in the platform code and report errors to Dart.

Practice Questions

  1. How does MethodChannel differ from EventChannel?
  2. What happens if the platform method is not implemented?
  3. How do you pass complex nested data through MethodChannel?
  4. What is the purpose of the channel name string?
  5. Challenge: Build a Flutter app that uses MethodChannel to access the device's flashlight (torch). Dart sends toggleFlashlight method with an on boolean parameter. Android and iOS implement turning the flashlight on/off. Handle errors when the device has no flashlight.

Mini Project

Build a device info app with native channels:

  • Dart sends getDeviceInfo method
  • Android returns: device model, Android version, screen size, battery level
  • iOS returns: device model, iOS version, screen size, battery level
  • Display all info in a Flutter UI
  • Add EventChannel for real-time battery level changes
  • Handle errors gracefully when data is unavailable

FAQ

Can I call Flutter methods from platform code?

Yes. Use MethodChannel in reverse. Create a channel in Dart, and from Android use flutterEngine.dartExecutor.binaryMessenger to send method calls to Dart.

What is the performance overhead of MethodChannel?

MethodChannel calls are async and serialized. Each call takes about 100-200 microseconds. For high-frequency calls (60fps), consider BasicMessageChannel with binary codec.

How do I pass custom objects?

Serialize them to Map<String, dynamic> on both sides. Use JSON serialization for complex object graphs.

Can I use native channels with Flutter web?

No. MethodChannel requires the Flutter engine. For web, use JS interop through dart:js or package:js.

How do I debug platform code?

Use Android Studio's debugger for Android and Xcode's debugger for iOS. Set breakpoints in the platform method handler.

What is Next

Now that you understand native channels, learn about FFI for C/C++ interop. Proceed to Flutter FFI for accessing native C libraries directly from Dart. Then explore Flutter Build Runner for Code Generation tools.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro