跳到主要内容

เนื้อหาภาษาไทย / 泰语内容

系统指南 内容已审核

เข้าใจเรื่อง Platform Channels ใน Flutter

บทความนี้อธิบายถึง Platform Channels ใน Flutter ซึ่งเป็นกลไกสำคัญที่ช่วยให้ Flutter สามารถสื่อสารกับ Native code ของแต่ละแพลตฟอร์ม (Android/iOS) ได้อย่างราบรื่น...

เข้าใจเรื่อง 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

  1. สร้าง MethodChannel ใน Dart: สร้าง Instance ของ MethodChannel โดยระบุ Channel Name (String) ที่จะใช้สื่อสารกับ Native code
  2. เรียกใช้ Method: ใช้ Method invokeMethod() เพื่อเรียก Method ใน Native code พร้อมส่ง Arguments (ถ้ามี)
  3. Implement Method ใน Native Code: ใน Native code (Java/Kotlin สำหรับ Android, Objective-C/Swift สำหรับ iOS) ให้ Implement Method ที่ตรงกับ Channel Name และ Method Name ที่เรียกจาก Dart
  4. ส่งผลลัพธ์กลับไปยัง Dart: ใน Native code ให้ส่งผลลัพธ์กลับไปยัง Dart ผ่าน Method result.success() (Android) หรือ result() (iOS)
  5. จัดการ 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 และเรียก Method getBatteryLevel
  • ใน 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

  1. สร้าง EventChannel ใน Dart: สร้าง Instance ของ EventChannel โดยระบุ Channel Name
  2. Listen to Stream: ใช้ Method receiveBroadcastStream() เพื่อ Listen to Stream ของ Event ที่ส่งมาจาก Native
  3. Implement EventChannel ใน Native Code: ใน Native code ให้ Implement EventChannel.StreamHandler (Android) หรือ FlutterStreamHandler (iOS)
  4. ส่ง Event ไปยัง Dart: ใน Native code ให้ส่ง Event ไปยัง Dart ผ่าน Method events.success() (Android) หรือ sink.success() (iOS)
  5. จัดการ 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 ได้ง่ายขึ้น

Office.in.th Custom Service

想基于此结构开发或定制系统?

Office.in.th 承接企业级 Web Application、API Integration 以及政府/企业管理系统的设计与开发,并提供长期支持。

查看软件开发服务