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.
MethodChanneluntuk pemanggilan satu kali dengan hasil: Dart memanggil → native proses → balikan hasil. GunakaninvokeMethod<T>()dengan tipe yang tepat.EventChanneluntuk stream data dari native ke Dart: sensor, status jaringan, notifikasi real-time. Native memanggileventSink.success()dan Dart mendengarkan viareceiveBroadcastStream().- Nama channel harus unik — gunakan format
com.namaapp/nama-channeluntuk 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
PlatformExceptiondi sisi Dart dan selalu panggilresult.success(),result.error(), atauresult.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 →