Platform Channels #

Flutter menjalankan kode Dart di atas mesin virtualnya sendiri, terpisah dari kode native Android dan iOS. Platform Channels adalah jembatan yang menghubungkan keduanya — memungkinkan Dart memanggil API native (Kotlin/Swift) dan sebaliknya. Ini dibutuhkan ketika kamu perlu mengakses fitur perangkat yang belum ada plugin Flutter-nya, atau mengintegrasikan SDK third-party yang hanya tersedia dalam format native.

Tiga Jenis Channel #

MethodChannel    -- pemanggilan satu arah dengan balasan sekali
                   Dart → Native → balikan hasil ke Dart
                   Contoh: ambil level baterai, baca IMEI, panggil SDK

EventChannel     -- stream data dari native ke Dart secara terus-menerus
                   Native → Dart (stream)
                   Contoh: sensor akselerometer, status jaringan real-time

BasicMessageChannel -- komunikasi dua arah dengan pesan berulang
                   Dart ↔ Native
                   Jarang digunakan; MethodChannel + EventChannel lebih umum

MethodChannel — Pemanggilan Sekali #

Sisi Dart #

// lib/core/platform/battery_service.dart
import 'package:flutter/services.dart';

class BatteryService {
  // Nama channel harus unik -- konvensi: domain/nama
  static const _channel = MethodChannel('com.contohapp/battery');

  // Ambil level baterai dari native
  static Future<int> getBatteryLevel() async {
    try {
      final level = await _channel.invokeMethod<int>('getBatteryLevel');
      return level ?? -1;
    } on PlatformException catch (e) {
      debugPrint('Error getBatteryLevel: ${e.message}');
      return -1;
    }
  }

  // Panggil native dengan argumen
  static Future<String?> getDeviceInfo({required bool includeModel}) async {
    try {
      return await _channel.invokeMethod<String>(
        'getDeviceInfo',
        {'includeModel': includeModel},  // argumen sebagai Map
      );
    } on PlatformException catch (e) {
      debugPrint('Error: ${e.code} - ${e.message}');
      return null;
    }
  }
}

Sisi Android (Kotlin) #

// android/app/src/main/kotlin/com/contohapp/MainActivity.kt
package com.contohapp

import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
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.contohapp/battery"

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

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                // Semua handler berjalan di main thread
                when (call.method) {
                    "getBatteryLevel" -> {
                        val level = getBatteryLevel()
                        if (level != -1) {
                            result.success(level)
                        } else {
                            result.error(
                                "UNAVAILABLE",
                                "Level baterai tidak tersedia",
                                null
                            )
                        }
                    }
                    "getDeviceInfo" -> {
                        val includeModel = call.argument<Boolean>("includeModel") ?: false
                        val info = buildString {
                            append("Android ${Build.VERSION.RELEASE}")
                            if (includeModel) append(" - ${Build.MODEL}")
                        }
                        result.success(info)
                    }
                    else -> result.notImplemented()
                }
            }
    }

    private fun getBatteryLevel(): Int {
        val batteryIntent = registerReceiver(
            null,
            IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        )
        return batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
    }
}

Sisi iOS (Swift) #

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

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    private let CHANNEL = "com.contohapp/battery"

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller = window?.rootViewController as! FlutterViewController
        let batteryChannel = FlutterMethodChannel(
            name: CHANNEL,
            binaryMessenger: controller.binaryMessenger
        )

        batteryChannel.setMethodCallHandler { [weak self] (call, result) in
            switch call.method {
            case "getBatteryLevel":
                self?.getBatteryLevel(result: result)
            case "getDeviceInfo":
                let args = call.arguments as? [String: Any]
                let includeModel = args?["includeModel"] as? Bool ?? false
                let info = "iOS \(UIDevice.current.systemVersion)" +
                    (includeModel ? " - \(UIDevice.current.model)" : "")
                result(info)
            default:
                result(FlutterMethodNotImplemented)
            }
        }

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    private func getBatteryLevel(result: FlutterResult) {
        UIDevice.current.isBatteryMonitoringEnabled = true
        let level = UIDevice.current.batteryLevel

        if level < 0 {
            result(FlutterError(
                code: "UNAVAILABLE",
                message: "Level baterai tidak tersedia",
                details: nil
            ))
        } else {
            result(Int(level * 100))
        }
    }
}

EventChannel — Stream Data Terus-Menerus #

Sisi Dart #

// lib/core/platform/sensor_service.dart
class SensorService {
  static const _eventChannel = EventChannel('com.contohapp/sensor');

  // Stream yang memancarkan data akselerometer secara terus-menerus
  static Stream<Map<String, double>> get accelerometerStream {
    return _eventChannel
        .receiveBroadcastStream()
        .map((dynamic event) {
          final map = event as Map<dynamic, dynamic>;
          return {
            'x': (map['x'] as num).toDouble(),
            'y': (map['y'] as num).toDouble(),
            'z': (map['z'] as num).toDouble(),
          };
        });
  }
}

// Penggunaan di widget
StreamBuilder<Map<String, double>>(
  stream: SensorService.accelerometerStream,
  builder: (context, snapshot) {
    if (!snapshot.hasData) return const Text('Menunggu data sensor...');
    final data = snapshot.data!;
    return Text('X: ${data['x']!.toStringAsFixed(2)}, '
                'Y: ${data['y']!.toStringAsFixed(2)}, '
                'Z: ${data['z']!.toStringAsFixed(2)}');
  },
)

Sisi Android (Kotlin) — EventChannel #

// Tambahkan EventChannel di configureFlutterEngine
private val EVENT_CHANNEL = "com.contohapp/sensor"
private var eventSink: EventChannel.EventSink? = null
private val handler = Handler(Looper.getMainLooper())
private val sensorRunnable = object : Runnable {
    var x = 0.0; var y = 0.0; var z = 0.0

    override fun run() {
        // Simulasi data sensor (di real app: gunakan SensorManager)
        x += 0.1; y += 0.05; z += 0.02
        eventSink?.success(mapOf("x" to x, "y" to y, "z" to z))
        handler.postDelayed(this, 100)  // emit setiap 100ms
    }
}

EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
    .setStreamHandler(object : EventChannel.StreamHandler {
        override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
            eventSink = sink
            handler.post(sensorRunnable)  // mulai kirim data
        }

        override fun onCancel(arguments: Any?) {
            handler.removeCallbacks(sensorRunnable)  // stop saat tidak ada listener
            eventSink = null
        }
    })

Sisi iOS (Swift) — EventChannel #

// Di AppDelegate, tambahkan EventChannel
private var eventSink: FlutterEventSink?
private var timer: Timer?

let sensorChannel = FlutterEventChannel(
    name: "com.contohapp/sensor",
    binaryMessenger: controller.binaryMessenger
)
sensorChannel.setStreamHandler(self)

// Extension untuk StreamHandler
extension AppDelegate: FlutterStreamHandler {
    func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
        self.eventSink = eventSink
        // Mulai kirim data sensor setiap 100ms
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
            eventSink(["x": Double.random(in: -1...1),
                       "y": Double.random(in: -1...1),
                       "z": Double.random(in: 9.5...10.0)])
        }
        return nil
    }

    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        timer?.invalidate()
        timer = nil
        eventSink = nil
        return nil
    }
}

Tipe Data yang Didukung #

Platform Channels menggunakan StandardMessageCodec yang mendukung tipe berikut:

Dart          →  Android (Kotlin)   →  iOS (Swift)
────────────────────────────────────────────────────
null          →  null               →  nil
bool          →  Boolean            →  Bool
int           →  Int                →  Int / Int32 / Int64
double        →  Double             →  Double
String        →  String             →  String
Uint8List     →  ByteArray          →  FlutterStandardTypedData
List<T>       →  ArrayList          →  Array
Map<K,V>      →  HashMap            →  Dictionary

Untuk tipe lain (objek kustom):
  → Konversi ke Map<String, dynamic> di sisi Dart
  → Baca dari Map di sisi native
  → Konversi kembali ke objek Dart saat kembali

Alternatif — Pigeon (Code Generation) #

Pigeon adalah tool resmi Flutter untuk generate type-safe platform channel code — tidak perlu menulis boilerplate manual:

// pigeons/messages.dart -- definisi interface
import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  dartOut: 'lib/src/messages.g.dart',
  kotlinOut: 'android/app/src/main/kotlin/.../Messages.g.kt',
  swiftOut: 'ios/Runner/Messages.g.swift',
))
@HostApi()  // native mengimplementasikan, Dart memanggil
abstract class BatteryApi {
  int getBatteryLevel();
  DeviceInfo getDeviceInfo(DeviceInfoRequest request);
}

class DeviceInfo {
  String osVersion = '';
  String? model;
}

class DeviceInfoRequest {
  bool includeModel = false;
}
# Generate code
dart run pigeon --input pigeons/messages.dart

Pigeon menghasilkan kode yang fully typed — tidak ada string method name, tidak ada cast manual, error langsung tertangkap saat compile time.


Ringkasan #

  • Platform Channels adalah jembatan antara Dart dan kode native — gunakan saat butuh fitur yang tidak ada plugin Flutter-nya atau mengintegrasikan SDK native.
  • MethodChannel untuk pemanggilan satu kali dengan hasil: Dart memanggil → native proses → balikan hasil. Gunakan invokeMethod<T>() dengan tipe yang tepat.
  • EventChannel untuk stream data dari native ke Dart: sensor, status jaringan, notifikasi real-time. Native memanggil eventSink.success() dan Dart mendengarkan via receiveBroadcastStream().
  • Nama channel harus unik — gunakan format com.namaapp/nama-channel untuk menghindari konflik dengan plugin lain.
  • Platform Channels hanya mendukung tipe data primitif dan koleksi — untuk objek kustom, konversi ke Map<String, dynamic> terlebih dahulu.
  • Selalu tangani PlatformException di sisi Dart dan selalu panggil result.success(), result.error(), atau result.notImplemented() di sisi native — jangan biarkan result tanpa respons.
  • Untuk proyek yang serius, gunakan Pigeon untuk generate type-safe channel code secara otomatis — menghindari bug dari string method name yang salah ketik.

← Sebelumnya: Performance Best Practice   Berikutnya: Background Tasks →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact