การสร้าง Plugin สำหรับ Flutter
ในบทความที่แล้ว เราได้เรียนรู้เกี่ยวกับ Platform Channels ซึ่งเป็นกลไกสำคัญที่ช่วยให้ Flutter สามารถสื่อสารกับ Native code ของแต่ละแพลตฟอร์มได้ ในบทความนี้ เราจะนำความรู้เรื่อง Platform Channels มาประยุกต์ใช้ในการสร้าง Plugin สำหรับ Flutter ซึ่งจะช่วยให้เราสามารถนำโค้ด Native ที่เขียนไว้แล้วมาใช้ซ้ำในหลายๆ แอปพลิเคชันได้อย่างง่ายดาย
Plugin คืออะไร?
Plugin ใน Flutter คือ Package ที่ประกอบไปด้วยโค้ด Dart และ Native code (เช่น Java/Kotlin สำหรับ Android, Objective-C/Swift สำหรับ iOS) ซึ่งช่วยให้ Flutter สามารถเข้าถึงฟังก์ชันการทำงานของอุปกรณ์หรือระบบปฏิบัติการที่ Flutter เองไม่สามารถทำได้โดยตรง ตัวอย่างเช่น การเข้าถึง Camera, GPS, Bluetooth หรือการใช้ Native UI Components
Plugin ช่วยให้เรา:
- นำโค้ด Native ที่มีอยู่แล้วมาใช้ซ้ำ
- เข้าถึงฟังก์ชันการทำงานของอุปกรณ์ที่ Flutter ยังไม่รองรับ
- สร้าง Component ที่มีประสิทธิภาพสูงซึ่งใช้ประโยชน์จาก Native APIs
โครงสร้าง Plugin
โครงสร้างพื้นฐานของ Plugin Flutter จะประกอบด้วย:
- `lib/`: โฟลเดอร์ที่เก็บโค้ด Dart ซึ่งเป็น Interface สำหรับ Plugin
- `android/`: โฟลเดอร์ที่เก็บโค้ด Native สำหรับ Android (Java/Kotlin)
- `ios/`: โฟลเดอร์ที่เก็บโค้ด Native สำหรับ iOS (Objective-C/Swift)
- `example/`: โฟลเดอร์ที่เก็บแอปพลิเคชันตัวอย่างที่ใช้ Plugin
- `pubspec.yaml`: ไฟล์ที่กำหนดข้อมูลของ Plugin เช่น ชื่อ, เวอร์ชั่น, dependencies
เราจะมาสร้าง Plugin อย่างง่ายเพื่อแสดงข้อความจาก Native code กัน
ขั้นตอนการสร้าง Plugin
- สร้าง Plugin Project: ใช้ Flutter CLI สร้าง Plugin Project โดยใช้คำสั่ง:
- แก้ไข `pubspec.yaml`: แก้ไข `pubspec.yaml` เพื่อกำหนดข้อมูลของ Plugin เช่น ชื่อ, คำอธิบาย, เวอร์ชั่น, และ dependencies
name: hello_plugin description: A new Flutter plugin project. version: 0.0.1 homepage: https://example.com environment: sdk: ">=2.12.0 <3.0.0" flutter: ">=1.20.0" dependencies: flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.0 flutter: plugin: platforms: android: package: com.example.hello_plugin pluginClass: HelloPlugin ios: pluginClass: HelloPlugin - เขียน Dart Interface ใน `lib/hello_plugin.dart`: สร้าง Class ที่เป็น Interface สำหรับ Plugin ของเรา
import 'dart:async'; import 'package:flutter/services.dart'; class HelloPlugin { static const MethodChannel _channel = const MethodChannel('hello_plugin'); static Future<String?> getPlatformVersion() async { final String? version = await _channel.invokeMethod('getPlatformVersion'); return version; } static Future<String?> getNativeGreeting(String name) async { final String? greeting = await _channel.invokeMethod('getNativeGreeting', {'name': name}); return greeting; } }ในโค้ดนี้ เราได้สร้าง `MethodChannel` ชื่อ `hello_plugin` และสร้าง Method 2 ตัวคือ `getPlatformVersion` และ `getNativeGreeting` ซึ่งจะถูกเรียกจาก Native code ในภายหลัง
- เขียน Native Code สำหรับ Android ใน `android/src/main/kotlin/com/example/hello_plugin/HelloPlugin.kt`: สร้าง Plugin Class ที่จะ Handle Method Calls จาก Flutter
package com.example.hello_plugin import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result /** HelloPlugin */ class HelloPlugin: FlutterPlugin, MethodCallHandler { private lateinit var channel : MethodChannel override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "hello_plugin") channel.setMethodCallHandler(this) } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "getPlatformVersion" -> { result.success("Android ${android.os.Build.VERSION.RELEASE}") } "getNativeGreeting" -> { val name = call.argument<String>("name") ?: "World" result.success("Hello, $name from Android!") } else -> { result.notImplemented() } } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } }ในโค้ดนี้ เราได้สร้าง `HelloPlugin` Class ที่ Implement `FlutterPlugin` และ `MethodCallHandler` interface และ Override Method `onMethodCall` เพื่อ Handle Method Calls จาก Flutter โดย Method `getPlatformVersion` จะ Return Android Version และ Method `getNativeGreeting` จะ Return Greeting Message จาก Android
- เขียน Native Code สำหรับ iOS ใน `ios/Classes/HelloPlugin.m`: สร้าง Plugin Class ที่จะ Handle Method Calls จาก Flutter
// HelloPlugin.m #import "HelloPlugin.h" @implementation HelloPlugin + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"hello_plugin" binaryMessenger:[registrar messenger]]; HelloPlugin* instance = [[HelloPlugin alloc] init]; [registrar addMethodCallDelegate:instance channel:channel]; } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@"getPlatformVersion" isEqualToString:call.method]) { result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); } else if ([@"getNativeGreeting" isEqualToString:call.method]) { NSString *name = call.arguments[@"name"] ?: @"World"; NSString *greeting = [NSString stringWithFormat:@"Hello, %@ from iOS!", name]; result(greeting); } else { result(FlutterMethodNotImplemented); } } @endในโค้ดนี้ เราได้สร้าง `HelloPlugin` Class ที่ Implement `FlutterPlugin` protocol และ Override Method `handleMethodCall` เพื่อ Handle Method Calls จาก Flutter โดย Method `getPlatformVersion` จะ Return iOS Version และ Method `getNativeGreeting` จะ Return Greeting Message จาก iOS
สำหรับ Swift ให้สร้างไฟล์ `ios/Classes/HelloPlugin.swift` และ `ios/hello_plugin.podspec` ที่ถูกต้อง
- ทดสอบ Plugin ใน Example App: แก้ไข `example/lib/main.dart` เพื่อทดสอบ Plugin ของเรา
import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:hello_plugin/hello_plugin.dart'; void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { String _platformVersion = 'Unknown'; String _greetingMessage = 'No Greeting Yet'; @override void initState() { super.initState(); initPlatformState(); } Future<void> initPlatformState() async { String platformVersion; String greetingMessage; try { platformVersion = await HelloPlugin.getPlatformVersion() ?? 'Unknown platform version'; greetingMessage = await HelloPlugin.getNativeGreeting("Flutter User") ?? 'No Greeting'; } on PlatformException { platformVersion = 'Failed to get platform version.'; greetingMessage = 'Failed to get greeting.'; } setState(() { _platformVersion = platformVersion; _greetingMessage = greetingMessage; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Hello Plugin example app'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Running on: $_platformVersion\n'), Text('Greeting: $_greetingMessage\n'), ], ), ), ), ); } }ในโค้ดนี้ เราได้เรียก Method `getPlatformVersion` และ `getNativeGreeting` จาก Plugin ของเรา และแสดงผลลัพธ์บนหน้าจอ
- Run Example App: รัน Example App เพื่อทดสอบ Plugin ของเรา
cd example flutter runคุณควรจะเห็น Platform Version และ Greeting Message ที่มาจาก Native Code บนหน้าจอ
flutter create --template=plugin hello_plugin
กรณีใช้งานจริงและข้อผิดพลาดที่พบบ่อย
- การจัดการ Error Handling: ควรมีการจัดการ Error Handling ที่ดีทั้งใน Dart และ Native Code เพื่อให้แอปพลิเคชันทำงานได้อย่างราบรื่น แม้ว่าจะเกิดข้อผิดพลาดขึ้น
- การจัดการ Asynchronous Operations: Method Calls ระหว่าง Flutter และ Native Code เป็น Asynchronous Operations ดังนั้นควรใช้ `async` และ `await` อย่างเหมาะสม
- การจัดการ Threading: ควรระมัดระวังเรื่อง Threading โดยเฉพาะใน Native Code เพื่อหลีกเลี่ยงปัญหา UI Blocking
- Dependency Management: ตรวจสอบให้แน่ใจว่า Dependencies ใน `pubspec.yaml` และ Native Build Files ถูกต้องและเป็นปัจจุบัน
- Platform-Specific Code: ทดสอบ Plugin บนทั้ง Android และ iOS เพื่อให้แน่ใจว่าทำงานได้อย่างถูกต้องบนทั้งสองแพลตฟอร์ม
- Versioning: จัดการ Version ของ Plugin อย่างเหมาะสม เพื่อให้ผู้ใช้งานสามารถอัปเดต Plugin ได้อย่างง่ายดาย
สรุป
การสร้าง Plugin สำหรับ Flutter เป็นวิธีที่มีประสิทธิภาพในการนำโค้ด Native ที่มีอยู่แล้วมาใช้ซ้ำ และเข้าถึงฟังก์ชันการทำงานของอุปกรณ์ที่ Flutter ยังไม่รองรับ โดยการใช้ Platform Channels เป็นกลไกหลักในการสื่อสารระหว่าง Flutter และ Native Code ในบทความนี้ เราได้เรียนรู้วิธีการสร้าง Plugin อย่างง่าย พร้อมทั้งกรณีใช้งานจริงและข้อผิดพลาดที่พบบ่อย ในบทความถัดไป เราจะพูดถึง GraphQL และการใช้งานแทน REST API ใน Flutter