เข้าใจเรื่อง Platform Channels ใน Flutter: สื่อสารกับ Native Code ของแต่ละ Platform
ในโลกของการพัฒนาแอปพลิเคชันข้ามแพลตฟอร์ม (Cross-Platform Development) บางครั้งเราก็จำเป็นต้องเข้าถึงฟีเจอร์เฉพาะของแต่ละแพลตฟอร์ม (Native Features) ที่ Flutter ยังไม่ได้รองรับโดยตรง หรือต้องการใช้ Library ที่เขียนด้วยภาษา Native เช่น Java/Kotlin สำหรับ Android หรือ Objective-C/Swift สำหรับ iOS นั่นเอง Flutter ได้เตรียมกลไกที่เรียกว่า "Platform Channels" เพื่อให้เราสามารถสื่อสารระหว่าง Dart code ใน Flutter กับ Native code ของแต่ละแพลตฟอร์มได้อย่างราบรื่น
บทความนี้จะพาคุณไปทำความเข้าใจ Platform Channels ใน Flutter อย่างละเอียด พร้อมตัวอย่างการใช้งานจริง และข้อควรระวังต่างๆ ที่ควรรู้
Platform Channels คืออะไร?
Platform Channels คือ ช่องทางสื่อสารระหว่าง Dart code (Flutter) และ Native code (Android/iOS) เปรียบเสมือนสะพานที่เชื่อมสองโลกเข้าด้วยกัน ทำให้เราสามารถเรียกใช้ฟังก์ชัน Native, รับข้อมูลจาก Native, หรือส่งข้อมูลจาก Flutter ไปยัง Native ได้
Flutter มี Platform Channels ให้เลือกใช้ 3 ประเภทหลักๆ ได้แก่:
- MethodChannel: ใช้สำหรับการเรียก Method จาก Dart ไปยัง Native และรับผลลัพธ์กลับมา เหมาะสำหรับการใช้งานทั่วไป เช่น เรียกใช้ฟังก์ชัน Native เพื่ออ่านค่า Sensor หรือทำการประมวลผลบางอย่าง
- EventChannel: ใช้สำหรับการส่ง Event จาก Native ไปยัง Dart อย่างต่อเนื่อง เหมาะสำหรับการใช้งานที่ต้องการรับข้อมูลแบบ Real-time เช่น การรับข้อมูล GPS coordinates หรือข้อมูลจาก Accelerometer
- BasicMessageChannel: ใช้สำหรับการส่ง Message แบบ Asynchronous ระหว่าง Dart และ Native เหมาะสำหรับการสื่อสารที่ซับซ้อนกว่า MethodChannel และ EventChannel
เราจะมาเจาะลึกแต่ละ Channel พร้อมตัวอย่างการใช้งานกัน
1. MethodChannel: เรียกใช้ Native Methods
MethodChannel เป็น Channel ที่ได้รับความนิยมมากที่สุด เนื่องจากใช้งานง่ายและครอบคลุมการใช้งานทั่วไป เราจะมาดูวิธีการใช้งาน MethodChannel แบบ Step-by-step กัน
ขั้นตอนการใช้งาน MethodChannel
- สร้าง MethodChannel ใน Dart: สร้าง Instance ของ
MethodChannelโดยระบุ Channel Name (String) ที่จะใช้สื่อสารกับ Native code - เรียกใช้ Method: ใช้ Method
invokeMethod()เพื่อเรียก Method ใน Native code พร้อมส่ง Arguments (ถ้ามี) - Implement Method ใน Native Code: ใน Native code (Java/Kotlin สำหรับ Android, Objective-C/Swift สำหรับ iOS) ให้ Implement Method ที่ตรงกับ Channel Name และ Method Name ที่เรียกจาก Dart
- ส่งผลลัพธ์กลับไปยัง Dart: ใน Native code ให้ส่งผลลัพธ์กลับไปยัง Dart ผ่าน Method
result.success()(Android) หรือresult()(iOS) - จัดการ Error (ถ้ามี): หากเกิด Error ใน Native code ให้ส่ง Error กลับไปยัง Dart ผ่าน Method
result.error()(Android) หรือresult()พร้อม Error Code และ Message (iOS)
ตัวอย่าง: การเรียกใช้ Native Method เพื่อรับ Battery Level
สมมติว่าเราต้องการรับ Battery Level ของอุปกรณ์จาก Native code เราสามารถทำได้ดังนี้:
Dart (Flutter) Code:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class BatteryLevelScreen extends StatefulWidget {
@override
_BatteryLevelScreenState createState() => _BatteryLevelScreenState();
}
class _BatteryLevelScreenState extends State<BatteryLevelScreen> {
static const platform = const MethodChannel('com.example.app/battery'); // Channel Name
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel'); // Method Name
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Battery Level'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_batteryLevel),
ElevatedButton(
onPressed: _getBatteryLevel,
child: Text('Get Battery Level'),
),
],
),
),
);
}
}
Android (Java) Code:
package com.example.app;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.MethodCall;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.example.app/battery";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
}
);
}
private int getBatteryLevel() {
int batteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(Context.BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
}
iOS (Swift) Code:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "com.example.app/battery",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self.receiveBatteryLevel(result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == UIDevice.BatteryState.unknown {
result(FlutterError(code: "UNAVAILABLE",
message: "Battery info unavailable",
details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}
คำอธิบาย:
- ใน Dart code เราสร้าง
MethodChannelชื่อcom.example.app/batteryและเรียก MethodgetBatteryLevel - ใน Android code เรา Implement
MethodCallHandlerเพื่อจัดการ Method Call ที่มาจาก Dart และส่ง Battery Level กลับไป - ใน iOS code เรา Implement
FlutterMethodChannelเพื่อจัดการ Method Call ที่มาจาก Dart และส่ง Battery Level กลับไป
ข้อผิดพลาดที่พบบ่อย
- Channel Name ไม่ตรงกัน: ตรวจสอบให้แน่ใจว่า Channel Name ใน Dart code และ Native code ตรงกัน
- Method Name ไม่ตรงกัน: ตรวจสอบให้แน่ใจว่า Method Name ใน Dart code และ Native code ตรงกัน
- PlatformException: หากเกิด Error ใน Native code จะถูกส่งกลับมาเป็น
PlatformExceptionใน Dart code ให้จัดการ Exception นี้อย่างเหมาะสม
2. EventChannel: ส่ง Event จาก Native ไปยัง Dart
EventChannel เหมาะสำหรับการส่งข้อมูลจาก Native ไปยัง Dart แบบ Real-time เช่น การรับข้อมูล GPS coordinates หรือข้อมูลจาก Accelerometer เราจะมาดูวิธีการใช้งาน EventChannel กัน
ขั้นตอนการใช้งาน EventChannel
- สร้าง EventChannel ใน Dart: สร้าง Instance ของ
EventChannelโดยระบุ Channel Name - Listen to Stream: ใช้ Method
receiveBroadcastStream()เพื่อ Listen to Stream ของ Event ที่ส่งมาจาก Native - Implement EventChannel ใน Native Code: ใน Native code ให้ Implement
EventChannel.StreamHandler(Android) หรือFlutterStreamHandler(iOS) - ส่ง Event ไปยัง Dart: ใน Native code ให้ส่ง Event ไปยัง Dart ผ่าน Method
events.success()(Android) หรือsink.success()(iOS) - จัดการ Error และ End-of-Stream: ใน Native code ให้จัดการ Error และ End-of-Stream ผ่าน Method
events.error()และevents.endOfStream()(Android) หรือsink.error()และsink.endOfStream()(iOS)
ตัวอย่าง: การรับ GPS Coordinates จาก Native
(ตัวอย่างนี้จะแสดงเฉพาะแนวคิดหลัก เนื่องจากโค้ด GPS ค่อนข้างซับซ้อนและต้องใช้ permission)
Dart (Flutter) Code:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LocationScreen extends StatefulWidget {
@override
_LocationScreenState createState() => _LocationScreenState();
}
class _LocationScreenState extends State<LocationScreen> {
static const eventChannel = const EventChannel('com.example.app/location');
String _location = 'Unknown location.';
@override
void initState() {
super.initState();
eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
void _onEvent(Object event) {
setState(() {
_location = 'Location: ${event.toString()}';
});
}
void _onError(Object error) {
setState(() {
_location = 'Error: ${error.toString()}';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Location'),
),
body: Center(
child: Text(_location),
),
);
}
}
Android (Java) Code:
// This is a simplified example. Real GPS code is more complex.
new EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setStreamHandler(
new EventChannel.StreamHandler() {
@Override
public void onListen(Object args, EventSink events) {
// Start listening to GPS updates
// ...
// When a new location is available:
events.success("Latitude: 12.345, Longitude: 67.890");
}
@Override
public void onCancel(Object args) {
// Stop listening to GPS updates
// ...
}
}
);
iOS (Swift) Code:
// This is a simplified example. Real GPS code is more complex.
let locationChannel = FlutterEventChannel(name: "com.example.app/location",
binaryMessenger: controller.binaryMessenger)
locationChannel.setStreamHandler(self)
// ...
extension AppDelegate: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink) -> FlutterError? {
// Start listening to GPS updates
// ...
// When a new location is available:
events("Latitude: 12.345, Longitude: 67.890")
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
// Stop listening to GPS updates
// ...
return nil
}
}
คำอธิบาย:
- ใน Dart code เราสร้าง
EventChannelชื่อcom.example.app/locationและ Listen to Stream ที่ส่งมาจาก Native - ใน Android code เรา Implement
EventChannel.StreamHandlerเพื่อจัดการการ Start และ Stop Listening to GPS updates และส่ง Location coordinates กลับไป - ใน iOS code เรา Implement
FlutterStreamHandlerเพื่อจัดการการ Start และ Stop Listening to GPS updates และส่ง Location coordinates กลับไป
3. BasicMessageChannel: ส่ง Message แบบ Asynchronous
BasicMessageChannel เป็น Channel ที่มีความยืดหยุ่นมากที่สุด สามารถใช้ส่ง Message แบบ Asynchronous ระหว่าง Dart และ Native ได้ เหมาะสำหรับการสื่อสารที่ซับซ้อนกว่า MethodChannel และ EventChannel
(ตัวอย่างการใช้งาน BasicMessageChannel จะไม่ถูกแสดงในบทความนี้ เนื่องจากมีการใช้งานที่ซับซ้อนกว่า และไม่ค่อยถูกใช้งานบ่อยนัก)
สรุป
Platform Channels เป็นเครื่องมือที่ทรงพลังในการเชื่อมต่อ Flutter กับ Native code ทำให้เราสามารถเข้าถึงฟีเจอร์เฉพาะของแต่ละแพลตฟอร์ม และใช้ประโยชน์จาก Library ที่เขียนด้วยภาษา Native ได้อย่างเต็มที่ การเลือกใช้ Channel ที่เหมาะสมกับ Use Case และการจัดการ Error อย่างถูกต้องเป็นสิ่งสำคัญเพื่อให้การสื่อสารระหว่าง Flutter และ Native เป็นไปอย่างราบรื่น
ในบทความถัดไป เราจะมาเรียนรู้เกี่ยวกับการสร้าง Plugin สำหรับ Flutter เพื่อ Encapsulate Platform Channels และ Reuse code ได้ง่ายขึ้น