Platform Channels #

Dalam ekosistem pengembangan aplikasi Flutter, kode Dart berjalan di atas mesin virtualnya sendiri (Dart Virtual Machine) yang terisolasi. Meskipun Flutter sangat andal dalam merender UI berkinerja tinggi secara lintas platform, terdapat skenario di mana aplikasi kita harus berinteraksi langsung dengan API sistem operasi tingkat rendah (low-level OS APIs) atau mengintegrasikan SDK pihak ketiga (third-party SDKs) yang hanya tersedia dalam format native. Beberapa contoh fitur ini meliputi pemeriksaan status enkripsi perangkat di Secure Enclave/Keystore, pembacaan data sensor perangkat keras, pengelolaan konektivitas Bluetooth tingkat lanjut, atau integrasi sistem pembayaran native.

Untuk menjembatani perbedaan lingkungan eksekusi antara Dart VM dan platform host (Android/iOS), Flutter menyediakan mekanisme yang disebut Platform Channels. Mekanisme ini memfasilitasi pertukaran pesan secara asinkronus melewati batas platform (platform boundary) dengan memanfaatkan saluran komunikasi berkecepatan tinggi yang aman.

Arsitektur dan Komunikasi Dasar Platform Channels #

Arsitektur Platform Channels bertumpu pada pengiriman pesan asinkron berbasis biner. Di lapisan terbawah, Flutter menggunakan komponen bernama BinaryMessenger untuk mengirimkan byte buffer mentah melintasi batas platform. Ketika kita memanggil metode melalui Platform Channel di sisi Dart, argumen tersebut dikodekan menjadi biner oleh kode pembuat kode (codec), dikirim melalui BinaryMessenger ke sisi native, didekodekan oleh codec native, dan akhirnya dieksekusi oleh pengendali platform.

Komunikasi ini berjalan secara asinkronus dan bersifat non-blocking untuk memastikan thread rendering utama (UI Thread) di sisi Dart tidak membeku saat menunggu respons dari operasi native yang mungkin membutuhkan waktu lama.

Berikut adalah diagram alur yang menunjukkan proses serialisasi, transmisi, dan deserialisasi data dari Dart menuju kode native Android/iOS melalui Platform Channels:

flowchart TD
    subgraph DartEnv["Lingkungan Dart (UI Thread)"]
        DartCode["Kode Dart (Service)"] -->|"invokeMethod(method, args)"| MethodChan["MethodChannel"]
        MethodChan -->|"encodeMethodCall()"| CodecDart["StandardMessageCodec (Dart)"]
        CodecDart -->|"Kirim byte buffer"| MessengerDart["BinaryMessenger (Dart)"]
    end

    subgraph NativeEnv["Lingkungan Native (Main Thread)"]
        MessengerNative["BinaryMessenger (Native)"] -->|"Terima byte buffer"| CodecNative["StandardMessageCodec (Native)"]
        CodecNative -->|"decodeMethodCall()"| HandlerNative["MethodCallHandler"]
        HandlerNative -->|"Kotlin / Swift API"| NativeAPIs["Sistem Operasi API"]
    end

    MessengerDart -->|Channel Boundary| MessengerNative
    NativeAPIs -->|"Hasil Native"| HandlerNative
    HandlerNative -->|"encodeSuccessEnvelope()"| CodecNative
    CodecNative -->|"Kirim byte buffer balik"| MessengerNative
    MessengerNative -->|Channel Boundary| MessengerDart
    MessengerDart -->|"Terima byte buffer balik"| CodecDart
    CodecDart -->|"decodeEnvelope()"| MethodChan
    MethodChan -->|"Return Future"| DartCode

Tiga Jenis Platform Channels #

Flutter menyediakan tiga jenis Platform Channel yang dirancang untuk skenario komunikasi yang berbeda:

  1. MethodChannel: Jenis saluran yang paling umum digunakan. Saluran ini dirancang untuk komunikasi request-response satu kali (seperti panggilan fungsi asinkron biasa). Dart mengirimkan pesan yang berisi nama metode dan argumen, lalu native memprosesnya dan mengembalikan satu respons balik ke Dart.
  2. EventChannel: Saluran ini dirancang khusus untuk aliran data berkelanjutan (continuous data streaming) secara asinkron dari platform native ke Dart. Sangat cocok untuk mengamati perubahan data sensor (akselerometer), mendengarkan perubahan status jaringan (WiFi/Seluler) secara real-time, atau menerima pembaruan lokasi GPS di background.
  3. BasicMessageChannel: Saluran komunikasi dua arah yang fleksibel untuk mengirimkan pesan berulang dengan tipe data mentah (seperti string atau byte) menggunakan codec kustom. Jenis ini lebih jarang digunakan karena fungsionalitasnya sebagian besar sudah dapat dipenuhi oleh kombinasi MethodChannel dan EventChannel.

StandardMessageCodec dan Serialisasi Data #

Sebelum data dikirim melalui BinaryMessenger, objek Dart harus diserialisasikan menjadi biner. Secara default, Flutter menggunakan StandardMessageCodec untuk menangani enkripsi dan dekripsi ini. Codec ini mendukung konversi otomatis untuk tipe data dasar berikut:

DartAndroid (Kotlin)iOS (Swift)
nullnullnil
booljava.lang.BooleanNSNumber (Boolean)
intjava.lang.Integer / LongNSNumber (Integer)
doublejava.lang.DoubleNSNumber (Double)
Stringjava.lang.StringString
Uint8Listbyte[]FlutterStandardTypedData
Int32Listint[]FlutterStandardTypedData
Listjava.util.ArrayListArray
Mapjava.util.HashMapDictionary

Jika kita ingin mengirimkan objek kustom (misalnya model data buatan kita sendiri), kita tidak bisa mengirimkannya secara langsung. Kita harus melakukan serialisasi objek tersebut menjadi Map<String, dynamic> di sisi Dart terlebih dahulu, mengirimkannya melalui channel, lalu mendekodenya di sisi native menjadi representasi objek native yang setara.


MethodChannel: Pemanggilan Sekali (Request-Response) #

Mari kita bedah implementasi mendalam penggunaan MethodChannel untuk mengambil data level baterai perangkat dan status pengisian dayanya. Kita akan menerapkan penanganan kesalahan (error handling) yang sangat ketat baik di sisi Dart maupun di sisi native (Kotlin untuk Android dan Swift untuk iOS).

1. Implementasi Sisi Dart #

Di sisi Dart, kita akan membuat sebuah kelas layanan yang membungkus pemanggilan MethodChannel. Kita harus memastikan nama channel yang digunakan bersifat unik (mengikuti konvensi domain terbalik) agar tidak bertabrakan dengan plugin lain di proyek kita.

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

class BatteryPlatformService {
  // Gunakan nama channel unik. Rekomendasi: domain/nama_fitur
  static const MethodChannel _channel = MethodChannel('com.unisbadri.flutter/battery');

  /// Mengambil persentase baterai perangkat saat ini (0-100).
  /// Mengembalikan nilai -1 jika terjadi kesalahan atau status tidak tersedia.
  static Future<int> dapatkanLevelBaterai() async {
    try {
      // invokeMethod mengembalikan Future yang harus ditunggu secara asinkron
      final int? hasil = await _channel.invokeMethod<int>('getBatteryLevel');
      return hasil ?? -1;
    } on PlatformException catch (e) {
      // Menangani error spesifik yang dilemparkan dari sisi native
      debugPrint('Gagal mengambil level baterai: Code: ${e.code}, Message: ${e.message}, Details: ${e.details}');
      return -1;
    } on MissingPluginException catch (e) {
      // Menangani error jika nama channel atau method di sisi native belum terdaftar
      debugPrint('Implementasi native tidak ditemukan: ${e.message}');
      return -1;
    } catch (e) {
      debugPrint('Error tidak terduga: $e');
      return -1;
    }
  }

  /// Mengambil informasi status pengisian daya baterai saat ini.
  static Future<String> dapatkanStatusCharger({required bool formatKapital}) async {
    try {
      final String? status = await _channel.invokeMethod<String>(
        'getBatteryStatus',
        {
          'formatKapital': formatKapital, // Mengirimkan argumen berupa Map
        },
      );
      return status ?? 'Tidak Diketahui';
    } on PlatformException catch (e) {
      debugPrint('Gagal mengambil status charger: ${e.message}');
      return 'Error: ${e.code}';
    }
  }
}

2. Implementasi Sisi Android (Kotlin) #

Pada sisi Android, kita mengimplementasikan handler di dalam MainActivity.kt. Kita perlu memperhatikan bahwa panggilan dari Dart secara default masuk ke main thread UI Android. Jika kita melakukan kalkulasi berat di dalam handler, kita wajib memindahkannya ke thread latar belakang menggunakan Kotlin Coroutines untuk mencegah pemblokiran aplikasi (Application Not Responding / ANR).

// android/app/src/main/kotlin/com/unisbadri/flutter/MainActivity.kt
package com.unisbadri.flutter

import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity: FlutterActivity() {
    private val BATTERY_CHANNEL = "com.unisbadri.flutter/battery"

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

        // Daftarkan MethodChannel
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL)
            .setMethodCallHandler { call, result ->
                // Pastikan penanganan eksekusi yang aman
                when (call.method) {
                    "getBatteryLevel" -> {
                        // Jalankan pengambilan baterai secara aman
                        val level = dapatkanPersentaseBaterai()
                        if (level != -1) {
                            result.success(level)
                        } else {
                            result.error(
                                "UNAVAILABLE",
                                "Persentase baterai tidak dapat dibaca dari sistem Android.",
                                null
                            )
                        }
                    }
                    "getBatteryStatus" -> {
                        // Mengambil argumen bertipe Boolean yang dikirim dari Dart
                        val formatKapital = call.argument<Boolean>("formatKapital") ?: false
                        
                        // Gunakan Coroutine jika proses memerlukan waktu lama atau akses I/O
                        CoroutineScope(Dispatchers.Main).launch {
                            val status = withContext(Dispatchers.IO) {
                                dapatkanStatusPengisianDaya(formatKapital)
                            }
                            result.success(status)
                        }
                    }
                    else -> {
                        // Beritahu Dart bahwa metode yang diminta tidak ada
                        result.notImplemented()
                    }
                }
            }
    }

    private fun dapatkanPersentaseBaterai(): Int {
        val batteryLevel: Int
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(
                null,
                IntentFilter(Intent.ACTION_BATTERY_CHANGED)
            )
            batteryLevel = if (intent != null) {
                val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
                val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
                if (level != -1 && scale != -1) {
                    (level * 100) / scale
                } else {
                    -1
                }
            } else {
                -1
            }
        }
        return batteryLevel
    }

    private fun dapatkanStatusPengisianDaya(formatKapital: Boolean): String {
        val intent = ContextWrapper(applicationContext).registerReceiver(
            null,
            IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        )
        val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
        val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                status == BatteryManager.BATTERY_STATUS_FULL

        val hasilText = if (isCharging) "sedang mengisi daya" else "tidak mengisi daya"
        return if (formatKapital) {
            hasilText.uppercase()
        } else {
            hasilText
        }
    }
}

3. Implementasi Sisi iOS (Swift) #

Di sisi iOS, kita menangani platform channel di berkas AppDelegate.swift. Kita harus menggunakan penanganan memori yang aman ([weak self]) saat mendaftarkan penangan panggilan agar tidak memicu kebocoran siklus referensi (strong reference cycles).

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

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    private let BATTERY_CHANNEL_NAME = "com.unisbadri.flutter/battery"

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        
        let batteryChannel = FlutterMethodChannel(
            name: BATTERY_CHANNEL_NAME,
            binaryMessenger: controller.binaryMessenger
        )
        
        // Daftarkan callback handler secara aman dengan [weak self]
        batteryChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
            guard let self = self else { return }
            
            switch call.method {
            case "getBatteryLevel":
                self.dapatkanLevelBaterai(result: result)
            case "getBatteryStatus":
                // Parsing argumen as? [String: Any]
                let arguments = call.arguments as? [String: Any]
                let formatKapital = arguments?["formatKapital"] as? Bool ?? false
                
                // Gunakan DispatchQueue jika melakukan kalkulasi berat latar belakang
                DispatchQueue.global(qos: .userInitiated).async {
                    let status = self.dapatkanStatusPengisianDaya(formatKapital: formatKapital)
                    
                    // Selalu kembalikan hasil ke thread utama iOS
                    DispatchQueue.main.async {
                        result(status)
                    }
                }
            default:
                result(FlutterMethodNotImplemented)
            }
        }
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    private func dapatkanLevelBaterai(result: FlutterResult) {
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        
        // Nilai batteryLevel berkisar antara 0.0 (kosong) hingga 1.0 (penuh)
        let levelBaterai = device.batteryLevel
        
        if levelBaterai < 0 {
            // Mengirimkan error spesifik ke Dart
            result(FlutterError(
                code: "UNAVAILABLE",
                message: "Tingkat baterai iOS tidak terdeteksi (kemungkinan berjalan di Simulator).",
                details: nil
            ))
        } else {
            result(Int(levelBaterai * 100))
        }
    }

    private func dapatkanStatusPengisianDaya(formatKapital: Bool) -> String {
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        
        let status = device.batteryState
        let isCharging = status == .charging || status == .full
        
        let hasilText = isCharging ? "sedang mengisi daya" : "tidak mengisi daya"
        return formatKapital ? hasilText.uppercased() : hasilText
    }
}

EventChannel: Aliran Data Terus-Menerus (Streaming) #

Untuk skenario pemantauan data asinkron berkelanjutan—seperti melacak pembaruan status konektivitas jaringan (Internet) perangkat—EventChannel adalah pilihan arsitektur yang paling tepat. Kita akan mengimplementasikan detektor jaringan dinamis yang memberi tahu aplikasi Dart secara real-time saat status jaringan berubah dari terkoneksi ke terputus, atau sebaliknya.

1. Implementasi Sisi Dart #

Di sisi Dart, kita akan mendengarkan aliran data (Stream) yang diekspos oleh EventChannel, melakukan pemetaan tipe data, dan memastikan bahwa kita mengelola daur hidup langganan stream tersebut agar tidak terjadi kebocoran memori.

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

enum StatusJaringan { terhubung, terputus, tidakDiketahui }

class NetworkStreamService {
  static const EventChannel _eventChannel = EventChannel('com.unisbadri.flutter/network_status');

  /// Stream yang memancarkan StatusJaringan terbaru secara real-time.
  static Stream<StatusJaringan> get statusJaringanStream {
    return _eventChannel
        .receiveBroadcastStream()
        .map((dynamic event) {
          // Konversi data mentah dari native menjadi status enum di Dart
          if (event is String) {
            switch (event) {
              case 'CONNECTED':
                return StatusJaringan.terhubung;
              case 'DISCONNECTED':
                return StatusJaringan.terputus;
            }
          }
          return StatusJaringan.tidakDiketahui;
        })
        .handleError((error) {
          debugPrint('Error pada EventChannel Jaringan: $error');
          return StatusJaringan.tidakDiketahui;
        });
  }
}

Berikut adalah contoh penggunaan NetworkStreamService di dalam widget kita menggunakan StreamBuilder:

// lib/presentation/widgets/status_jaringan_widget.dart
import 'package:flutter/material.dart';
import '../../core/platform/network_stream_service.dart';

class StatusJaringanWidget extends StatelessWidget {
  const StatusJaringanWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<StatusJaringan>(
      stream: NetworkStreamService.statusJaringanStream,
      initialData: StatusJaringan.tidakDiketahui,
      builder: (context, snapshot) {
        final status = snapshot.data ?? StatusJaringan.tidakDiketahui;
        
        Color warnaStatus = Colors.grey;
        String teksStatus = 'Memeriksa Jaringan...';

        if (status == StatusJaringan.terhubung) {
          warnaStatus = Colors.green;
          teksStatus = 'ONLINE';
        } else if (status == StatusJaringan.terputus) {
          warnaStatus = Colors.red;
          teksStatus = 'OFFLINE';
        }

        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          decoration: BoxDecoration(
            color: warnaStatus.withOpacity(0.15),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: warnaStatus),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Icon(
                status == StatusJaringan.terhubung ? Icons.wifi : Icons.wifi_off,
                color: warnaStatus,
              ),
              const SizedBox(width: 8),
              Text(
                teksStatus,
                style: TextStyle(color: warnaStatus, fontWeight: FontWeight.bold),
              ),
            ],
          ),
        );
      },
    );
  }
}

2. Sisi Android (Kotlin) — EventChannel #

Di Android, kita mengimplementasikan antarmuka EventChannel.StreamHandler. Kita mendaftarkan BroadcastReceiver secara dinamis saat Dart mendengarkan stream, dan menghapusnya segera ketika Dart membatalkan langganan stream tersebut guna mencegah kebocoran referensi konteks Android.

// android/app/src/main/kotlin/com/unisbadri/flutter/MainActivity.kt (Lanjutan)
package com.unisbadri.flutter

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.NetworkInfo
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel

class MainActivity: FlutterActivity() {
    private val NETWORK_CHANNEL = "com.unisbadri.flutter/network_status"
    private var networkReceiver: BroadcastReceiver? = null

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

        EventChannel(flutterEngine.dartExecutor.binaryMessenger, NETWORK_CHANNEL)
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                    if (events == null) return

                    // Kirim status awal saat pertama kali didengarkan
                    events.success(dapatkanStatusKoneksiSaatIni())

                    // Buat receiver dinamis untuk mendengarkan perubahan status jaringan
                    networkReceiver = object : BroadcastReceiver() {
                        override fun onReceive(context: Context?, intent: Intent?) {
                            events.success(dapatkanStatusKoneksiSaatIni())
                        }
                    }

                    registerReceiver(
                        networkReceiver,
                        IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
                    )
                }

                override fun onCancel(arguments: Any?) {
                    // Bersihkan receiver untuk mencegah kebocoran memori context Android
                    if (networkReceiver != null) {
                        unregisterReceiver(networkReceiver)
                        networkReceiver = null
                    }
                }
            })
    }

    private fun dapatkanStatusKoneksiSaatIni(): String {
        val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
        val isConnected = activeNetwork?.isConnectedOrConnecting == true
        return if (isConnected) "CONNECTED" else "DISCONNECTED"
    }
}

3. Sisi iOS (Swift) — EventChannel #

Di iOS, kita menggunakan NWPathMonitor dari framework Network untuk mengamati perubahan konektivitas perangkat. Kita harus berhati-hati untuk memulai dan membatalkan monitor ini pada daur hidup handler yang tepat.

// ios/Runner/AppDelegate.swift (Lanjutan)
import UIKit
import Flutter
import Network

class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
    private let NETWORK_CHANNEL_NAME = "com.unisbadri.flutter/network_status"
    
    // Inisialisasi properti pemantau jaringan
    private var monitor: NWPathMonitor?
    private var eventSink: FlutterEventSink?
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        
        let networkChannel = FlutterEventChannel(
            name: NETWORK_CHANNEL_NAME,
            binaryMessenger: controller.binaryMessenger
        )
        networkChannel.setStreamHandler(self)
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // Dipanggil saat Dart mulai mendengarkan stream
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.eventSink = events
        self.monitor = NWPathMonitor()
        
        // Daftarkan callback pemantau perubahan status
        self.monitor?.pathUpdateHandler = { [weak self] path in
            guard let self = self, let sink = self.eventSink else { return }
            
            let statusString = (path.status == .satisfied) ? "CONNECTED" : "DISCONNECTED"
            
            // Selalu kirim pembaruan kembali ke Main Thread
            DispatchQueue.main.async {
                sink(statusString)
            }
        }
        
        // Jalankan monitor pada antrean latar belakang
        let queue = DispatchQueue(label: "NetworkMonitorQueue")
        self.monitor?.start(queue: queue)
        
        return nil
    }

    // Dipanggil saat Dart menghentikan pemantauan stream
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        // Matikan monitor secara bersih untuk membebaskan resource sistem
        self.monitor?.cancel()
        self.monitor = nil
        self.eventSink = nil
        return nil
    }
}

BasicMessageChannel: Komunikasi Dua Arah Terbuka #

BasicMessageChannel dirancang untuk skenario di mana kita membutuhkan saluran komunikasi asinkron dua arah yang persisten untuk mengirimkan pesan berseri secara terus-menerus. Saluran ini memanfaatkan objek MessageCodec untuk mengontrol konversi format data.

Berikut adalah implementasi sederhana pertukaran string JSON mentah antara Dart dan Native menggunakan BasicMessageChannel.

1. Sisi Dart #

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

class MessageSyncService {
  // Gunakan StringCodec untuk mengirimkan pesan berupa teks langsung
  static const BasicMessageChannel<String> _messageChannel =
      BasicMessageChannel<String>('com.unisbadri.flutter/sync_message', StringCodec());

  static void inisialisasiPenerimaPesan() {
    // Menghandle pesan masuk dari native ke Dart
    _messageChannel.setMessageHandler((String? message) async {
      print('Menerima pesan dari Native: $message');
      return 'Diterima oleh Dart'; // Balasan opsional ke native
    });
  }

  static Future<void> kirimPesanKeNative(String payloadJson) async {
    // Mengirim pesan dari Dart ke native
    final String? balasan = await _messageChannel.send(payloadJson);
    print('Balasan dari Native: $balasan');
  }
}

2. Sisi Android (Kotlin) #

// Inisialisasi BasicMessageChannel di MainActivity.kt
val messageChannel = BasicMessageChannel(
    flutterEngine.dartExecutor.binaryMessenger,
    "com.unisbadri.flutter/sync_message",
    StringCodec.INSTANCE
)

// Mengatur penerima pesan dari Dart
messageChannel.setMessageHandler { message, reply ->
    println("Menerima dari Dart: $message")
    
    // Mengirim balasan instan ke Dart
    reply.reply("Android menerima pesan Anda.")
}

// Contoh mengirim pesan ke Dart secara asinkron
messageChannel.send("Pesan dari background Android") { reply ->
    println("Konfirmasi Dart: $reply")
}

3. Sisi iOS (Swift) #

// Inisialisasi BasicMessageChannel di AppDelegate.swift
let messageChannel = FlutterBasicMessageChannel(
    name: "com.unisbadri.flutter/sync_message",
    binaryMessenger: controller.binaryMessenger,
    codec: FlutterStringCodec.sharedInstance()
)

// Mengatur penerima pesan dari Dart
messageChannel.setMessageHandler { (message, reply) in
    print("Menerima dari Dart: \(String(describing: message))")
    
    // Mengirim balasan ke Dart
    reply("iOS menerima pesan Anda.")
}

// Mengirim pesan ke Dart secara asinkron
messageChannel.sendMessage("Pesan dari background iOS") { (reply) in
    print("Konfirmasi Dart: \(String(describing: reply))")
}

Pigeon: Alternatif Bertipe Aman (Type-Safe Code Generation) #

Meskipun menulis kode Platform Channels manual sangat fleksibel, pendekatan ini memiliki kelemahan besar pada proyek skala menengah hingga besar:

  1. Raw String Matching: Kita rentan membuat kesalahan ketik (typo) pada nama metode atau nama parameter Map. Kesalahan ini hanya terdeteksi saat runtime, bukan saat kompilasi.
  2. Manual Casting: Kita harus menulis kode boilerplate untuk mem-parsing data dari Map atau List secara manual baik di sisi Dart maupun native, yang berisiko memicu kesalahan tipe (type casting errors).

Untuk mengatasi kelemahan ini, tim Flutter menyediakan alat alternatif bernama Pigeon. Pigeon adalah alat generator kode (code generation) yang memungkinkan kita mendefinisikan skema API platform channel kita menggunakan berkas antarmuka (interface) Dart standar, lalu secara otomatis menghasilkan kelas penangan bertipe aman (strongly-typed) untuk Dart, Kotlin (Android), dan Swift (iOS).

Berikut adalah langkah-langkah implementasi terperinci menggunakan Pigeon:

Langkah 1: Tambahkan Dependensi Pigeon #

Tambahkan package Pigeon di bagian dev_dependencies berkas pubspec.yaml kita:

dev_dependencies:
  flutter_test:
    sdk: flutter
  pigeon: ^22.3.0

Langkah 2: Definisikan Antarmuka Skema API #

Buat berkas definisi skema baru, misalnya di folder pigeons/battery_api.dart:

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

// Konfigurasi tujuan output file kode generator
@ConfigurePigeon(PigeonOptions(
  dartOut: 'lib/src/generated/battery_api.g.dart',
  kotlinOut: 'android/app/src/main/kotlin/com/unisbadri/flutter/BatteryApi.g.kt',
  kotlinOptions: KotlinOptions(package: 'com.unisbadri.flutter'),
  swiftOut: 'ios/Runner/BatteryApi.g.swift',
))

// Model data terstruktur bertipe aman
class InformasiSistem {
  final String namaPerangkat;
  final String versiOs;
  final int levelBaterai;

  InformasiSistem({
    required this.namaPerangkat,
    required this.versiOs,
    required this.levelBaterai,
  });
}

// Antarmuka API yang akan diimplementasikan di sisi Native dan dipanggil oleh Dart
@HostApi()
abstract class BatteryPlatformApi {
  InformasiSistem ambilInformasiSistem();
}

Langkah 3: Jalankan Perintah Generator Pigeon #

Eksekusi generator Pigeon melalui konsol terminal proyek kita:

dart run pigeon --input pigeons/battery_api.dart

Perintah di atas akan menghasilkan berkas generator berisi kelas-kelas boilerplate bertipe aman di lokasi folder lib/src/generated/.

Langkah 4: Implementasikan Antarmuka Native #

A. Sisi Android (Kotlin) #

Kompilator Pigeon menghasilkan antarmuka BatteryPlatformApi. Kita cukup mengimplementasikan antarmuka ini dan mendaftarkannya di MainActivity.kt.

// android/app/src/main/kotlin/com/unisbadri/flutter/MainActivity.kt
package com.unisbadri.flutter

import android.os.Build
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

class MainActivity: FlutterActivity(), BatteryPlatformApi {

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        // Hubungkan implementasi kelas kita ke Pigeon Host API
        BatteryPlatformApi.setUp(flutterEngine.dartExecutor.binaryMessenger, this)
    }

    override fun ambilInformasiSistem(): InformasiSistem {
        val levelBaterai = 90 // Simulasi kalkulasi level baterai
        return InformasiSistem(
            namaPerangkat = Build.MODEL,
            versiOs = "Android " + Build.VERSION.RELEASE,
            levelBaterai = levelBaterai.toLong()
        )
    }
}

B. Sisi iOS (Swift) #

Di iOS, kita melakukan implementasi protokol BatteryPlatformApi yang telah digenerasikan Swift.

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

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate, BatteryPlatformApi {
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        
        // Daftarkan implementasi class AppDelegate ke Pigeon setup
        BatteryPlatformApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: self)
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    func ambilInformasiSistem() -> InformasiSistem {
        return InformasiSistem(
            namaPerangkat: UIDevice.current.name,
            versiOs: "iOS " + UIDevice.current.systemVersion,
            levelBaterai: Int64(95) // Simulasi kalkulasi level baterai
        )
    }
}

Langkah 5: Panggil API Pigeon di Sisi Dart #

Kini di sisi Dart, kita cukup memanggil instansi API tanpa perlu mencemaskan parsing string manual atau konversi Map yang rentan error:

// lib/core/platform/pigeon_battery_service.dart
import '../../src/generated/battery_api.g.dart';

class PigeonBatteryService {
  static final BatteryPlatformApi _api = BatteryPlatformApi();

  static Future<void> printInformasiPerangkat() async {
    try {
      // Panggilan fungsi bertipe aman langsung mengembalikan objek InformasiSistem
      final InformasiSistem info = await _api.ambilInformasiSistem();
      
      print('Nama Perangkat : ${info.namaPerangkat}');
      print('Versi OS       : ${info.versiOs}');
      print('Level Baterai  : ${info.levelBaterai}%');
    } catch (e) {
      print('Gagal mengambil data dari Pigeon: $e');
    }
  }
}

Praktik Terbaik Kinerja dan Penanganan Kesalahan #

Berikut adalah aturan dan prinsip utama yang harus kita terapkan saat mendesain Platform Channels pada proyek produksi:

1. Pindahkan Komputasi Berat ke Thread Latar Belakang (Native Threading) #

Secara default, seluruh pemanggilan handler Platform Channel di sisi Android (Kotlin/Java) dan iOS (Swift/Objective-C) berjalan di thread UI utama (Main Platform Thread). Jika handler native kita melakukan operasi komputasi berat, manipulasi citra, pembacaan database besar, atau komunikasi jaringan yang lambat, hal tersebut akan memblokir rendering UI utama sistem operasi, yang menyebabkan aplikasi tampak macet.

  • Android: Gunakan Kotlin Coroutines dengan Dispatchers Dispatchers.IO atau Java thread executor untuk memindahkan pemrosesan dari thread utama.
  • iOS: Gunakan Grand Central Dispatch (GCD) untuk mengirimkan pekerjaan berat ke antrean latar belakang (Dispatchers.global(qos: .background)), namun pastikan kita selalu memanggil result() callback kembali di thread utama (Dispatchers.main.async).

2. Hindari Pengiriman Objek Biner Besar Secara Berulang #

StandardMessageCodec memiliki overhead kinerja saat menyalin data biner melintasi batas memori platform channel. Jika kita perlu memproses file gambar besar, aliran video kamera, atau database biner mentah, hindari mengirimkannya sebagai array byte raksasa melintasi platform channel. Sebagai solusinya, simpan file tersebut di ruang penyimpanan lokal, kirimkan referensi jalur file (file path string) melintasi platform channel, lalu biarkan sisi native membaca data file tersebut secara lokal dari media penyimpanan.

3. Jaga Kebersihan Pengelolaan Resource (Memory Leak Prevention) #

Ketika menggunakan EventChannel, kegagalan membatalkan pendaftaran listener atau receiver native di sisi handler saat stream tidak lagi digunakan akan menyebabkan memory leak yang berbahaya. Konteks aktivitas Android atau pemantau internal iOS tidak akan pernah bisa dihapus dari heap memori karena masih dipegang secara aktif oleh event sink. Pastikan kita mengimplementasikan pembebasan memori, pembatalan timer, dan unregistrasi receiver di dalam metode onCancel() dari antarmuka stream handler native kita.

4. Terapkan Skema Nama Channel yang Konsisten dan Unik #

Bila kita mengembangkan plugin kustom, hindari nama channel yang terlalu generik seperti battery atau sensor. Gunakan format DNS terbalik yang terstruktur:

$$\text{Format Name} = \text{domain_perusahaan} + \text{"/"} + \text{nama_aplikasi} + \text{"/"} + \text{nama_layanan}$$

Contoh: com.companyname.appname/sensor_acceleration.


Ringkasan #

  • Platform Channels berfungsi sebagai jembatan asinkronus antara lingkungan eksekusi Dart VM dan sistem operasi native Android/iOS melalui transmisi pesan biner BinaryMessenger.
  • MethodChannel digunakan untuk interaksi request-response sekali panggil. Selalu gunakan penanganan tipe data yang tepat dan tangkap pengecualian PlatformException di sisi Dart.
  • EventChannel dirancang untuk streaming data berkelanjutan dari native ke Dart. Sangat penting untuk mematikan sensor, membatalkan monitor, dan melakukan unregistrasi receiver di dalam callback onCancel native guna mencegah kebocoran memori.
  • Pigeon adalah alat generator kode resmi Flutter yang wajib digunakan pada proyek skala besar untuk menghindari bug runtime akibat kesalahan ketik nama string atau konversi data tipe yang tidak aman.
  • Aturan Threading: Pindahkan selalu tugas-tugas native yang memakan resource berat (I/O, enkripsi, parsing data besar) dari thread utama native menggunakan Kotlin Coroutines atau Swift GCD agar tidak memblokir UI thread utama.

← Sebelumnya: Performance Best Practice   Berikutnya: Background Tasks →

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