Platform channel

Mengenal interoperabilitas kode native melalui platform channel.

Daftar isi

Flutter menggambar seluruh antarmukanya sendiri, tetapi tidak semua kemampuan perangkat dapat dijangkau dari kode Dart. Topik ini membahas platform channel, yakni mekanisme resmi untuk bertukar pesan antara kode Dart dan kode native (kode yang ditulis dalam bahasa bawaan platform, misalnya Kotlin untuk Android atau Swift untuk iOS). Pembahasan mencakup kapan kode native dibutuhkan, cara kerja MethodChannel pada dua arah panggilan, serta sifat asinkron dan aturan penamaan yang menyertainya. Setelah topik ini dibaca, keputusan menulis kode native sendiri dapat diambil secara terukur.

Kapan kode native dibutuhkan#

Kode native diperlukan apabila aplikasi mengakses kemampuan yang hanya disediakan sistem operasi dan belum tersedia sebagai package apa pun. Contoh yang umum dijumpai adalah pembacaan sensor khusus, akses layanan yang menuntut izin sistem, dan SDK (software development kit, perangkat lunak pengembangan) tertentu yang pustakanya hanya tersedia dalam bentuk native. Sebagian besar kebutuhan yang lazim sudah dijawab oleh package plugin di pub.dev, seperti kamera dan penyimpanan, sehingga penulisan kode native menjadi pilihan yang digunakan apabila plugin yang tersedia tidak ada atau perilakunya kurang sesuai.

Penulisan kode native menambah tanggung jawab baru. Setiap platform memerlukan implementasinya sendiri dalam bahasanya sendiri, sehingga satu fitur dapat ditulis dua kali untuk Android dan iOS. Pemeliharaan juga meluas ke luar Dart karena perubahan API pada setiap rilis sistem operasi harus diikuti. Karena itu, batasi kode native pada kemampuan yang memang tidak dapat dilakukan dari Dart.

Cara kerja platform channel#

Platform channel bekerja dengan menukar pesan, bukan dengan berbagi memori. Kode Dart dan kode native berjalan pada utas (thread, unit eksekusi) yang berbeda, sehingga satu-satunya jalur komunikasi adalah pengiriman pesan melalui binary messenger, yaitu lapisan transport bawaan Flutter. Setiap channel diberi satu nama unik, misalnya aplikasi_catatan/baterai, dan nama inilah yang menjadi alamat agar pesan tiba pada penerima yang tepat.

Isi pesan diterjemahkan oleh codec standar (standard message codec), yaitu aturan pengubahan tipe data dasar seperti angka, String, List, dan Map menjadi bentuk biner, dan hasil binernya diterjemahkan kembali di sisi penerima. Tipe di luar daftar itu, misalnya objek kelas buatan sendiri, harus dikonversi terlebih dahulu menjadi Map sebelum dikirim. Seluruh panggilan juga bersifat asinkron karena perjalanan pesan memakan waktu yang tidak dapat dipastikan, sehingga hasilnya selalu dibungkus Future.

Diagram berikut merangkum alur pesan pada platform channel. Kedua sisi tidak berbagi satu pun variabel, dan pertukaran hanya terjadi melalui pesan yang melintasi batas platform.

batas platformAplikasi Flutter (Dart)MethodChannelaplikasi_catatan/bateraisetMethodCallHandlerpenerima panggilan dari nativesemua panggilan asinkron (Future)Sisi nativeAndroid (Kotlin)MethodChannel handleriOS (Swift)FlutterMethodChannelsetiap platform menulis implementasinya sendiriinvokeMethod()pesan asinkronresult / callbackhasil atau error
Alur pesan platform channel dari Dart ke sisi native dan kembali

MethodChannel dari Dart ke native#

Arah panggilan yang paling sering digunakan adalah Dart meminta data atau layanan kepada kode native. Kelas yang menyediakannya bernama MethodChannel. Objeknya dibuat dengan satu nama channel, kemudian setiap permintaan dikirim melalui metode invokeMethod yang menerima nama metode beserta argumen opsional.

Potongan berikut membungkus channel persentase baterai. Nama metodenya persenBaterai, dan hasilnya berupa int yang berasal dari sisi native. Karena invokeMethod mengembalikan nilai yang bisa saja kosong, hasilnya diperiksa sebelum digunakan.

dart Contoh: pembungkus MethodChannel pada sisi Dart
// tidak-dijalankan
import 'package:flutter/services.dart';

class LayananBaterai {
  static const _channel = MethodChannel('aplikasi_catatan/baterai');

  static Future<int> persenBaterai() async {
    final hasil = await _channel.invokeMethod<int>('persenBaterai');
    if (hasil == null) {
      throw PlatformException(
        code: 'TANPA_DATA',
        message: 'Sisi native tidak mengembalikan nilai',
      );
    }
    return hasil;
  }
}

Pada sisi Android, handler didaftarkan pada MainActivity. Nama channel harus sama persis dengan yang ditulis di Dart, sebab nama yang berbeda membuat pesan tidak pernah sampai. Handler menerima nama metode beserta argumennya, lalu menjawab melalui objek result.

kotlin Contoh: handler MethodChannel pada Android (Kotlin)
// tidak-dijalankan
class MainActivity : FlutterActivity() {
  private val channelName = "aplikasi_catatan/baterai"

  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
      .setMethodCallHandler { call, result ->
        when (call.method) {
          "persenBaterai" -> {
            val batteryManager = getSystemService(BatteryManager::class.java)
            val level = batteryManager.getIntProperty(
              BatteryManager.BATTERY_PROPERTY_CAPACITY,
            )
            result.success(level)
          }
          else -> result.notImplemented()
        }
      }
  }
}

Permintaan yang tidak ditangani menghasilkan error di sisi Dart. Apabila metode yang diminta memang tidak dikenal, handler yang baik menjawab notImplemented sehingga Dart menerima MissingPluginException yang jelas alasannya, bukan menunggu tanpa jawaban.

Arah panggilan dari native ke Dart#

Arah sebaliknya juga terjadi, terutama ketika kode native perlu memberi tahu peristiwa kepada Dart, misalnya SDK peta yang melaporkan pergeseran kamera. Kode native memanggil invokeMethod pada channel yang sama, sedangkan sisi Dart mendaftarkan penerimanya dengan setMethodCallHandler. Tanggung jawab membalas berpindah ke sisi Dart, karena handler Dart wajib menjawab melalui result agar kode native tidak menunggu tanpa batas waktu.

dart Contoh: menerima panggilan dari native pada sisi Dart
// tidak-dijalankan
_channel.setMethodCallHandler((call) async {
  if (call.method == 'kameraBerpindah') {
    final titik = call.arguments as Map;
    // perbarui state layar dengan isi titik.
  }
  return null;
});

Satu channel dapat digunakan untuk dua arah, tetapi banyak pengembang memisahkan nama channel untuk setiap arah agar handler lebih mudah dibaca. Yang tetap harus dijaga hanyalah konsistensi nama di kedua sisi. Arah ini paling sering dibutuhkan ketika SDK native mengirimkan kejadian yang tidak diminta Dart, misalnya perubahan lokasi atau keadaan koneksi. Pada Android, panggilan dari native ke Dart juga wajib dilakukan pada utas utama, dan handler Dart yang tidak mengenali metodenya dapat menolak panggilan dengan melempar error, sebagaimana handler native menjawab notImplemented pada arah sebaliknya.

EventChannel untuk aliran data berkelanjutan#

MethodChannel mengirim permintaan yang berhenti setelah satu jawaban, sedangkan data yang berulang seperti perubahan baterai atau pembacaan sensor lebih cocok dengan EventChannel. EventChannel menghubungkan aliran peristiwa di sisi native dengan Stream (aliran data) di sisi Dart. Dart berlangganan stream tersebut sekali, lalu menerima setiap peristiwa yang dikirim native sampai langganannya dibatalkan. Pola ini menghindari polling, yakni pengecekan berulang yang boros, karena sisi native justru yang aktif mengirim ketika ada data baru. Kemampuan yang sudah tersedia sebagai plugin pun dibangun dengan pola yang sama, karena hampir semua plugin sensor bekerja dengan pola stream.

Langganan mengembalikan StreamSubscription yang sebaiknya dibatalkan pada metode dispose widget, karena langganan yang ditinggalkan membuat native terus mengirim peristiwa tanpa penerima yang menggunakannya. Penamaan EventChannel mengikuti kebiasaan MethodChannel, sehingga pendaftarannya hanya berganti kelas, sedangkan nama channel dan pola jawabannya sama.

Kesalahan umum#

Empat kesalahan yang paling sering muncul saat platform channel dipraktikkan:

  • Nama channel berbeda antara Dart dan native. Pesan dikirim ke alamat yang tidak terdaftar, sehingga Dart menerima MissingPluginException seolah-olah plugin tidak pernah dipasang. Bandingkan kedua nama karakter demi karakter, termasuk huruf besar-kecilnya.
  • Menganggap invokeMethod sinkron. Nilai kembaliannya adalah Future, bukan data, sehingga setiap panggilan wajib di-await sebelum hasilnya digunakan.
  • Mengirim objek kelas buatan sendiri tanpa konversi. Codec standar tidak mengenali kelas tersebut, sehingga pesan gagal diterjemahkan. Ubah objek menjadi Map terlebih dahulu.
  • Menjalankan pekerjaan berat di dalam handler native. Handler Android berjalan pada utas utama, sehingga pekerjaan yang lama membuat antarmuka tersendat. Pindahkan pekerjaan berat ke utas latar.

Ringkasan#

  • Platform channel menukar pesan antara Dart dan native melalui binary messenger, dengan nama channel sebagai alamat yang wajib konsisten di kedua sisi.
  • MethodChannel melayani permintaan dan jawaban pada dua arah, sedangkan EventChannel meneruskan aliran peristiwa berkelanjutan sebagai Stream.
  • Codec standar hanya mengenal tipe data dasar, sehingga objek kustom dikonversi menjadi Map terlebih dahulu.
  • Seluruh panggilan bersifat asinkron dan hasilnya dibungkus Future.

Referensi#