DartFlutter

Writing Native Platform Channels in Flutter Plugins

Writing Native Platform Channels In Flutter Plugins 683x1024

Introduction

Flutter provides a rich set of cross-platform APIs. However, some features still require direct access to native Android or iOS code—whether it’s integrating a device-specific SDK, accessing hardware sensors, or reusing existing native libraries. Platform channels make this possible by allowing Flutter to communicate with native code safely and efficiently. In this comprehensive guide, you will learn how platform channels work under the hood, implement all three channel types with production-ready code, handle complex data types and errors, and design clean, maintainable native integrations. By the end, you will understand how to extend Flutter beyond its built-in capabilities.

Understanding Platform Channel Architecture

Platform channels use a message-passing system between Dart and the host platform. Messages are encoded using a binary codec, sent across the platform boundary, decoded, and processed by handlers on each side.

┌─────────────────────────────────────────────────────────────────┐
│                        Flutter App (Dart)                        │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │              Platform Channel (Dart side)                  │ │
│  │  • MethodChannel - Request/Response calls                  │ │
│  │  • EventChannel  - Continuous data streams                 │ │
│  │  • BasicMessageChannel - Custom message passing            │ │
│  └─────────────────────────┬──────────────────────────────────┘ │
└────────────────────────────┼────────────────────────────────────┘
                             │ Binary Message
                             │ (StandardMessageCodec)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Platform Binary Messenger                     │
└─────────────────────────────────────────────────────────────────┘
                             │
          ┌──────────────────┴──────────────────┐
          ▼                                     ▼
┌─────────────────────┐             ┌─────────────────────┐
│   Android (Kotlin)  │             │    iOS (Swift)      │
│  ┌───────────────┐  │             │  ┌───────────────┐  │
│  │ Method Handler│  │             │  │ Method Handler│  │
│  └───────────────┘  │             │  └───────────────┘  │
│  ┌───────────────┐  │             │  ┌───────────────┐  │
│  │ Native APIs   │  │             │  │ Native APIs   │  │
│  │ • Camera      │  │             │  │ • HealthKit   │  │
│  │ • Bluetooth   │  │             │  │ • Core ML     │  │
│  │ • Sensors     │  │             │  │ • ARKit       │  │
│  └───────────────┘  │             │  └───────────────┘  │
└─────────────────────┘             └─────────────────────┘

Types of Platform Channels

MethodChannel

Used for one-time request/response method calls. This is the most common channel type.

  • Invoke native methods from Dart
  • Receive single responses asynchronously
  • Ideal for API calls, data retrieval, and actions

EventChannel

Used for continuous data streams from native to Dart.

  • Sends multiple events over time
  • Ideal for sensors, location updates, and real-time data
  • Supports broadcast-style updates

BasicMessageChannel

Used for custom message passing with custom codecs.

  • Supports bidirectional messaging
  • Flexible for custom protocols
  • Less commonly used than the others

Creating a Flutter Plugin

# Create a new plugin with Kotlin and Swift support
flutter create --template=plugin \
  --platforms=android,ios \
  --org com.yourcompany \
  device_info_plugin

cd device_info_plugin

This generates a complete plugin structure:

device_info_plugin/
├── lib/
│   ├── device_info_plugin.dart           # Dart API
│   ├── device_info_plugin_platform_interface.dart
│   └── device_info_plugin_method_channel.dart
├── android/
│   └── src/main/kotlin/.../DeviceInfoPlugin.kt
├── ios/
│   └── Classes/DeviceInfoPlugin.swift
├── example/                               # Example app
└── pubspec.yaml

Complete MethodChannel Implementation

Dart API Layer

// lib/device_info_plugin.dart
import 'dart:async';
import 'package:flutter/services.dart';

/// Device information retrieved from native platform
class DeviceInfo {
  final String platform;
  final String osVersion;
  final String deviceModel;
  final String deviceId;
  final int totalMemory;
  final int availableMemory;
  final double batteryLevel;
  final bool isPhysicalDevice;

  DeviceInfo({
    required this.platform,
    required this.osVersion,
    required this.deviceModel,
    required this.deviceId,
    required this.totalMemory,
    required this.availableMemory,
    required this.batteryLevel,
    required this.isPhysicalDevice,
  });

  factory DeviceInfo.fromMap(Map map) {
    return DeviceInfo(
      platform: map['platform'] as String,
      osVersion: map['osVersion'] as String,
      deviceModel: map['deviceModel'] as String,
      deviceId: map['deviceId'] as String,
      totalMemory: map['totalMemory'] as int,
      availableMemory: map['availableMemory'] as int,
      batteryLevel: (map['batteryLevel'] as num).toDouble(),
      isPhysicalDevice: map['isPhysicalDevice'] as bool,
    );
  }
}

/// Plugin for accessing device information
class DeviceInfoPlugin {
  static const MethodChannel _channel = MethodChannel(
    'com.yourcompany/device_info',
  );

  /// Get comprehensive device information
  static Future getDeviceInfo() async {
    try {
      final Map? result =
          await _channel.invokeMethod('getDeviceInfo');

      if (result == null) {
        throw PlatformException(
          code: 'NULL_RESPONSE',
          message: 'Native platform returned null',
        );
      }

      return DeviceInfo.fromMap(result);
    } on PlatformException catch (e) {
      throw DeviceInfoException(
        code: e.code,
        message: e.message ?? 'Unknown error',
        details: e.details,
      );
    }
  }

  /// Get platform version string
  static Future getPlatformVersion() async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version ?? 'Unknown';
  }

  /// Check if device is physical or emulator
  static Future isPhysicalDevice() async {
    final bool? isPhysical = await _channel.invokeMethod('isPhysicalDevice');
    return isPhysical ?? false;
  }

  /// Get battery level (0.0 - 1.0)
  static Future getBatteryLevel() async {
    final double? level = await _channel.invokeMethod('getBatteryLevel');
    return level ?? -1.0;
  }

  /// Open native settings
  static Future openSettings({String? section}) async {
    final bool? result = await _channel.invokeMethod(
      'openSettings',
      {'section': section},
    );
    return result ?? false;
  }

  /// Save data to native secure storage
  static Future saveSecureData({
    required String key,
    required String value,
  }) async {
    await _channel.invokeMethod('saveSecureData', {
      'key': key,
      'value': value,
    });
  }

  /// Retrieve data from native secure storage
  static Future getSecureData(String key) async {
    return await _channel.invokeMethod('getSecureData', {'key': key});
  }
}

/// Custom exception for device info errors
class DeviceInfoException implements Exception {
  final String code;
  final String message;
  final dynamic details;

  DeviceInfoException({
    required this.code,
    required this.message,
    this.details,
  });

  @override
  String toString() => 'DeviceInfoException($code): $message';
}

Android Implementation (Kotlin)

// android/src/main/kotlin/.../DeviceInfoPlugin.kt
package com.yourcompany.device_info_plugin

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.BatteryManager
import android.os.Build
import android.provider.Settings
import androidx.annotation.NonNull
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.*
import java.util.*

class DeviceInfoPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
    private lateinit var channel: MethodChannel
    private lateinit var context: Context
    private var activity: Activity? = null
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

    override fun onAttachedToEngine(
        @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
    ) {
        context = flutterPluginBinding.applicationContext
        channel = MethodChannel(
            flutterPluginBinding.binaryMessenger,
            "com.yourcompany/device_info"
        )
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        when (call.method) {
            "getPlatformVersion" -> {
                result.success("Android ${Build.VERSION.RELEASE}")
            }

            "getDeviceInfo" -> {
                scope.launch {
                    try {
                        val info = withContext(Dispatchers.IO) {
                            getDeviceInfoMap()
                        }
                        result.success(info)
                    } catch (e: Exception) {
                        result.error(
                            "DEVICE_INFO_ERROR",
                            e.message,
                            e.stackTraceToString()
                        )
                    }
                }
            }

            "isPhysicalDevice" -> {
                val isPhysical = !isEmulator()
                result.success(isPhysical)
            }

            "getBatteryLevel" -> {
                val batteryLevel = getBatteryLevel()
                result.success(batteryLevel)
            }

            "openSettings" -> {
                val section = call.argument("section")
                val opened = openSettings(section)
                result.success(opened)
            }

            "saveSecureData" -> {
                val key = call.argument("key")
                val value = call.argument("value")
                if (key != null && value != null) {
                    scope.launch(Dispatchers.IO) {
                        try {
                            saveToSecureStorage(key, value)
                            withContext(Dispatchers.Main) {
                                result.success(null)
                            }
                        } catch (e: Exception) {
                            withContext(Dispatchers.Main) {
                                result.error("SECURE_STORAGE_ERROR", e.message, null)
                            }
                        }
                    }
                } else {
                    result.error("INVALID_ARGS", "Key and value are required", null)
                }
            }

            "getSecureData" -> {
                val key = call.argument("key")
                if (key != null) {
                    scope.launch(Dispatchers.IO) {
                        try {
                            val value = getFromSecureStorage(key)
                            withContext(Dispatchers.Main) {
                                result.success(value)
                            }
                        } catch (e: Exception) {
                            withContext(Dispatchers.Main) {
                                result.error("SECURE_STORAGE_ERROR", e.message, null)
                            }
                        }
                    }
                } else {
                    result.error("INVALID_ARGS", "Key is required", null)
                }
            }

            else -> result.notImplemented()
        }
    }

    private fun getDeviceInfoMap(): Map {
        val runtime = Runtime.getRuntime()
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE)
                as android.app.ActivityManager
        val memoryInfo = android.app.ActivityManager.MemoryInfo()
        activityManager.getMemoryInfo(memoryInfo)

        return mapOf(
            "platform" to "Android",
            "osVersion" to Build.VERSION.RELEASE,
            "deviceModel" to "${Build.MANUFACTURER} ${Build.MODEL}",
            "deviceId" to getDeviceId(),
            "totalMemory" to memoryInfo.totalMem,
            "availableMemory" to memoryInfo.availMem,
            "batteryLevel" to getBatteryLevel(),
            "isPhysicalDevice" to !isEmulator(),
            "sdkVersion" to Build.VERSION.SDK_INT,
            "brand" to Build.BRAND,
            "board" to Build.BOARD,
            "hardware" to Build.HARDWARE
        )
    }

    private fun getDeviceId(): String {
        return Settings.Secure.getString(
            context.contentResolver,
            Settings.Secure.ANDROID_ID
        ) ?: UUID.randomUUID().toString()
    }

    private fun isEmulator(): Boolean {
        return (Build.FINGERPRINT.startsWith("generic")
                || Build.FINGERPRINT.startsWith("unknown")
                || Build.MODEL.contains("google_sdk")
                || Build.MODEL.contains("Emulator")
                || Build.MODEL.contains("Android SDK built for x86")
                || Build.MANUFACTURER.contains("Genymotion")
                || Build.BRAND.startsWith("generic")
                || Build.DEVICE.startsWith("generic")
                || Build.PRODUCT == "sdk"
                || Build.PRODUCT == "google_sdk"
                || Build.PRODUCT == "sdk_gphone_x86")
    }

    private fun getBatteryLevel(): Double {
        val batteryManager = context.getSystemService(Context.BATTERY_SERVICE)
                as BatteryManager
        val level = batteryManager.getIntProperty(
            BatteryManager.BATTERY_PROPERTY_CAPACITY
        )
        return level / 100.0
    }

    private fun openSettings(section: String?): Boolean {
        return try {
            val intent = when (section) {
                "wifi" -> Intent(Settings.ACTION_WIFI_SETTINGS)
                "bluetooth" -> Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
                "location" -> Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
                "notification" -> Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
                    putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
                }
                else -> Intent(Settings.ACTION_SETTINGS)
            }
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            context.startActivity(intent)
            true
        } catch (e: Exception) {
            false
        }
    }

    private fun getEncryptedPrefs(): android.content.SharedPreferences {
        val masterKey = MasterKey.Builder(context)
            .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
            .build()

        return EncryptedSharedPreferences.create(
            context,
            "secure_prefs",
            masterKey,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )
    }

    private fun saveToSecureStorage(key: String, value: String) {
        getEncryptedPrefs().edit().putString(key, value).apply()
    }

    private fun getFromSecureStorage(key: String): String? {
        return getEncryptedPrefs().getString(key, null)
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        scope.cancel()
        channel.setMethodCallHandler(null)
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = binding.activity
    }

    override fun onDetachedFromActivityForConfigChanges() {
        activity = null
    }

    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
        activity = binding.activity
    }

    override fun onDetachedFromActivity() {
        activity = null
    }
}

iOS Implementation (Swift)

// ios/Classes/DeviceInfoPlugin.swift
import Flutter
import UIKit
import Security

public class DeviceInfoPlugin: NSObject, FlutterPlugin {
    private var channel: FlutterMethodChannel?

    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(
            name: "com.yourcompany/device_info",
            binaryMessenger: registrar.messenger()
        )
        let instance = DeviceInfoPlugin()
        instance.channel = channel
        registrar.addMethodCallDelegate(instance, channel: channel)
    }

    public func handle(
        _ call: FlutterMethodCall,
        result: @escaping FlutterResult
    ) {
        switch call.method {
        case "getPlatformVersion":
            result("iOS \(UIDevice.current.systemVersion)")

        case "getDeviceInfo":
            DispatchQueue.global(qos: .userInitiated).async {
                let info = self.getDeviceInfoMap()
                DispatchQueue.main.async {
                    result(info)
                }
            }

        case "isPhysicalDevice":
            result(!isSimulator())

        case "getBatteryLevel":
            UIDevice.current.isBatteryMonitoringEnabled = true
            let level = UIDevice.current.batteryLevel
            result(Double(level))

        case "openSettings":
            if let args = call.arguments as? [String: Any],
               let section = args["section"] as? String {
                openSettings(section: section, result: result)
            } else {
                openSettings(section: nil, result: result)
            }

        case "saveSecureData":
            if let args = call.arguments as? [String: Any],
               let key = args["key"] as? String,
               let value = args["value"] as? String {
                let success = saveToKeychain(key: key, value: value)
                if success {
                    result(nil)
                } else {
                    result(FlutterError(
                        code: "KEYCHAIN_ERROR",
                        message: "Failed to save to keychain",
                        details: nil
                    ))
                }
            } else {
                result(FlutterError(
                    code: "INVALID_ARGS",
                    message: "Key and value are required",
                    details: nil
                ))
            }

        case "getSecureData":
            if let args = call.arguments as? [String: Any],
               let key = args["key"] as? String {
                let value = getFromKeychain(key: key)
                result(value)
            } else {
                result(FlutterError(
                    code: "INVALID_ARGS",
                    message: "Key is required",
                    details: nil
                ))
            }

        default:
            result(FlutterMethodNotImplemented)
        }
    }

    private func getDeviceInfoMap() -> [String: Any] {
        UIDevice.current.isBatteryMonitoringEnabled = true

        let processInfo = ProcessInfo.processInfo
        let device = UIDevice.current

        return [
            "platform": "iOS",
            "osVersion": device.systemVersion,
            "deviceModel": getDeviceModel(),
            "deviceId": getDeviceId(),
            "totalMemory": Int(processInfo.physicalMemory),
            "availableMemory": getAvailableMemory(),
            "batteryLevel": Double(device.batteryLevel),
            "isPhysicalDevice": !isSimulator(),
            "systemName": device.systemName,
            "name": device.name,
            "model": device.model,
            "localizedModel": device.localizedModel
        ]
    }

    private func getDeviceModel() -> String {
        var systemInfo = utsname()
        uname(&systemInfo)
        let machineMirror = Mirror(reflecting: systemInfo.machine)
        let identifier = machineMirror.children.reduce("") { identifier, element in
            guard let value = element.value as? Int8, value != 0 else {
                return identifier
            }
            return identifier + String(UnicodeScalar(UInt8(value)))
        }
        return identifier
    }

    private func getDeviceId() -> String {
        return UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
    }

    private func isSimulator() -> Bool {
        #if targetEnvironment(simulator)
        return true
        #else
        return false
        #endif
    }

    private func getAvailableMemory() -> Int {
        var taskInfo = task_vm_info_data_t()
        var count = mach_msg_type_number_t(
            MemoryLayout.size
        ) / 4
        let result: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
            $0.withMemoryRebound(
                to: integer_t.self,
                capacity: 1
            ) {
                task_info(
                    mach_task_self_,
                    task_flavor_t(TASK_VM_INFO),
                    $0,
                    &count
                )
            }
        }
        if result == KERN_SUCCESS {
            return Int(ProcessInfo.processInfo.physicalMemory) -
                   Int(taskInfo.phys_footprint)
        }
        return 0
    }

    private func openSettings(
        section: String?,
        result: @escaping FlutterResult
    ) {
        var urlString: String

        switch section {
        case "wifi":
            urlString = "App-Prefs:root=WIFI"
        case "bluetooth":
            urlString = "App-Prefs:root=Bluetooth"
        case "location":
            urlString = "App-Prefs:root=Privacy&path=LOCATION"
        case "notification":
            urlString = UIApplication.openSettingsURLString
        default:
            urlString = UIApplication.openSettingsURLString
        }

        DispatchQueue.main.async {
            if let url = URL(string: urlString),
               UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url, options: [:]) { success in
                    result(success)
                }
            } else {
                result(false)
            }
        }
    }

    // MARK: - Keychain Operations

    private func saveToKeychain(key: String, value: String) -> Bool {
        let data = value.data(using: .utf8)!

        // Delete existing item first
        let deleteQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        SecItemDelete(deleteQuery as CFDictionary)

        // Add new item
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]

        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }

    private func getFromKeychain(key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        if status == errSecSuccess,
           let data = result as? Data,
           let value = String(data: data, encoding: .utf8) {
            return value
        }
        return nil
    }
}

EventChannel Implementation

EventChannels are perfect for streaming continuous data like sensor readings:

// lib/battery_stream.dart
import 'dart:async';
import 'package:flutter/services.dart';

class BatteryStream {
  static const EventChannel _eventChannel = EventChannel(
    'com.yourcompany/battery_stream',
  );

  static Stream get batteryStateStream {
    return _eventChannel.receiveBroadcastStream().map((event) {
      final map = Map.from(event as Map);
      return BatteryState(
        level: (map['level'] as num).toDouble(),
        isCharging: map['isCharging'] as bool,
        temperature: (map['temperature'] as num?)?.toDouble(),
      );
    });
  }
}

class BatteryState {
  final double level;
  final bool isCharging;
  final double? temperature;

  BatteryState({
    required this.level,
    required this.isCharging,
    this.temperature,
  });
}
// Android EventChannel handler (Kotlin)
class BatteryStreamHandler(private val context: Context) : EventChannel.StreamHandler {
    private var eventSink: EventChannel.EventSink? = null
    private var batteryReceiver: BroadcastReceiver? = null

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        eventSink = events
        
        batteryReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                intent?.let {
                    val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
                    val scale = it.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
                    val status = it.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
                    val temp = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
                    
                    val batteryPct = level * 100 / scale.toFloat()
                    val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                                     status == BatteryManager.BATTERY_STATUS_FULL
                    
                    eventSink?.success(mapOf(
                        "level" to batteryPct / 100.0,
                        "isCharging" to isCharging,
                        "temperature" to temp / 10.0
                    ))
                }
            }
        }
        
        val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        context.registerReceiver(batteryReceiver, filter)
    }

    override fun onCancel(arguments: Any?) {
        batteryReceiver?.let { context.unregisterReceiver(it) }
        batteryReceiver = null
        eventSink = null
    }
}

Type-Safe Channels with Pigeon

For larger plugins, use Pigeon to generate type-safe platform channel code:

// pigeons/messages.dart
import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  dartOut: 'lib/src/messages.g.dart',
  kotlinOut: 'android/src/main/kotlin/Messages.g.kt',
  swiftOut: 'ios/Classes/Messages.g.swift',
))
class DeviceInfo {
  String? platform;
  String? osVersion;
  String? deviceModel;
  double? batteryLevel;
  bool? isPhysicalDevice;
}

@HostApi()
abstract class DeviceInfoApi {
  DeviceInfo getDeviceInfo();
  String getPlatformVersion();
  bool isPhysicalDevice();
  @async
  double getBatteryLevel();
}

@FlutterApi()
abstract class DeviceInfoFlutterApi {
  void onBatteryLevelChanged(double level);
}
# Generate type-safe code
flutter pub run pigeon --input pigeons/messages.dart

Common Mistakes to Avoid

Mistake 1: Mismatched Channel Names

// WRONG - Channel names don't match
// Dart
const MethodChannel _channel = MethodChannel('my_plugin');

// Kotlin
MethodChannel(messenger, "myPlugin") // Different name!

// CORRECT - Exact match
// Dart
const MethodChannel _channel = MethodChannel('com.company/my_plugin');

// Kotlin
MethodChannel(messenger, "com.company/my_plugin")

Mistake 2: Blocking the Main Thread

// WRONG - Heavy work on main thread
override fun onMethodCall(call: MethodCall, result: Result) {
    val data = heavyComputation() // Blocks UI!
    result.success(data)
}

// CORRECT - Use coroutines or background thread
override fun onMethodCall(call: MethodCall, result: Result) {
    scope.launch {
        val data = withContext(Dispatchers.IO) {
            heavyComputation()
        }
        result.success(data)
    }
}

Mistake 3: Generic Error Messages

// WRONG - Unhelpful error
result.error("ERROR", "Something went wrong", null)

// CORRECT - Descriptive error with details
result.error(
    "PERMISSION_DENIED",
    "Camera permission not granted",
    mapOf(
        "requestedPermission" to "android.permission.CAMERA",
        "currentStatus" to "denied"
    )
)

Conclusion

Platform channels allow Flutter plugins to communicate directly with native Android and iOS code, unlocking access to device APIs, native SDKs, and platform-specific features. By using MethodChannel for request/response calls and EventChannel for continuous streams, you can extend Flutter apps while keeping Dart APIs clean and intuitive. For complex plugins, consider using Pigeon to generate type-safe code and reduce boilerplate.

For push notification integration, read Push Notifications in Flutter with Firebase FCM. For offline-first patterns, see Building Offline-First Flutter Apps. Reference the official Flutter platform channels documentation and Pigeon package for advanced patterns.

Leave a Comment