# เขียน Native มาใช้บน Flutter
#Dev#Flutter#Pluginบางครั้งเราอาจจะต้องเขียน Plugin ที่ยืมความสามารถของ Native ขึ้นมาใช้เองถ้าไม่มีคนเขียนไว้ให้ ซึ่งแน่นอนว่าการจะเขียนได้นั้นต้องพอมีความรู้ในการเขียน Native สักนิดสักหน่อย ไม่ว่าจะเป็นภาษา Kotlin (Android) หรือ Swift (iOS) ซึ่งผมพอเข้าใจ Kotlin บ้างเล็กน้อย...ส่วน Swift นี่ไม่รู้เรื่องเลย 🤣
ในบทความนี้จะสอนวิธีเขียนตั้งแต่ต้น และพยายามอธิบายให้เข้าใจเขียนเองได้ครับ
# เตรียมความพร้อมสิ่งที่ควรมี
- Android Studio ที่ติดตั้ง Flutter Plugin แล้ว
- Visual Studio Code ที่ติดตั้ง Flutter Plugin แล้ว
# พื้นฐานของ Plugin ใน Flutter
หลักการแบบสั้นๆก็คือ เราจะทำการเปิดช่องให้ Flutter และ Native คุยกันผ่าน MethodChannel
# สร้างโปรเจค Flutter Plugin
เปิด Android Studio ขึ้นมาแล้วกดสร้างโปรเจค Flutter
เลือกสร้าง Flutter Plugin
ในที่นี้ผมจะทำ Plugin เรียกใช้ Toast (เป็น Alert เล็กๆ...เพื่อใครไม่รู้)
ที่สำคัญคืออย่าลืมติ๊ก Kotlin เพราะเราจะเขียนด้วย Kotlin เสร็จแล้วก็ Finish ได้เลยครับ
คลิกเลือกไฟล์ main.dart
แล้วลองรันดู จะต้องได้โปรแกรมหน้าตาตามนี้
# เริ่มเขียน Plugin
ไฟล์ที่เราสนใจจะมี lib/toast.dart
ที่เป็นไฟล์ของ Plugin ในฝั่ง Flutter แล้วก็ไฟล์ android/src/main/.../ToastPlugin.kt
เป็นไฟล์ Plugin ในฝั่ง Native Android แล้วก็ไฟล์ example/lib/main.dart
ที่เป็นไฟล์สำหรับใช้เขียนตัวอย่างการใช้งานของ Plugin ของเรา ส่วนไฟล์ test/toast_test.dart
เป็นไฟล์ทำ Testing ก็เอาเท่าที่สะดวกเลยครับ 🤣
# ฝั่ง Flutter
lib/toast.dart
import 'dart:async';
import 'package:flutter/services.dart';
class Toast {
static const MethodChannel _channel = const MethodChannel('toast');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
}
จากไฟล์ข้างต้นเราจะเห็นว่ามีการสร้าง MethodChannel ขึ้นมา คิดเสียว่าเป็นท่อท่อหนึ่งที่จะต่อไปยังฝั่ง Native
static const MethodChannel _channel = const MethodChannel('toast');
และ การเรียกใช้ getter platformVersion
นั้นจะไปสั่ง _channel.invokeMethod('getPlatformVersion')
เพื่อเรียกคำสั่ง getPlatformVersion
ในฝั่ง Native
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion'); // ฟังก์ชัน .invokeMethod คืนค่าเป็น Future เราต้องใส่ await ด้วย
return version;
}
เวลาเราจะใช้งานก็ง่ายๆเลยเพราะ getter ที่สร้างมาก็เป็น static แล้วเราเพียงแค่
var someVar = await Toast.platformVersion; // platformVersion คืนค่าเป็น Future เราต้องใส่ await ด้วย
เมื่อเราเรียกใช้ platformVersion
สิ่งที่เกิดขึ้นก็คือ Flutter จะสั่งคำสั่ง getPlatformVersion
ผ่านท่อที่มีชื่อว่า toast
ผมเปลี่ยนใหม่ ลบ platformVersion
ที่เขาให้มาแล้วจะสร้างฟังก์ชันสำหรับใช้ Toast กันครับ
import 'dart:async';
import 'package:flutter/services.dart';
class Toast {
static const MethodChannel _channel = const MethodChannel('toast');
static Future<void> toast() async {
_channel.invokeMethod('callAwesomeToast');
}
}
จากโค้ดไม่มีอะไรมาก เราแค่ต้องการสั่งคำสั่ง และการเรียก Toast ก็ไม่น่าจะต้องรับค่าอะไร return type จึงเป็น Future<void>
โค้ดฝั่ง Flutter มีแค่นี้ครับสั้นมาก 😎
# ฝั่ง Native
ทีนี้มาดูในฝั่ง Native กันบ้าง android/src/main/.../ToastPlugin.kt
(ถ้ามัน Error อะไรก็ไม่ต้องไปสนใจมัน)
package me.intception.toast
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
import io.flutter.plugin.common.PluginRegistry.Registrar
class ToastPlugin: MethodCallHandler {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "toast")
channel.setMethodCallHandler(ToastPlugin())
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
}
registerWith
เป็นส่วนที่ Plugin จะต้องมี
ซึ่งเมื่อเรารันโปรแกรม Flutter จะทำการเรียก ToastPlugin.registerWith(...)
เสมอ
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "toast")
channel.setMethodCallHandler(ToastPlugin())
}
}
โดยฟังก์ชันนี้เราจะทำการเปิดท่อของทางฝั่ง Native (แน่นอนว่าชื่อต้องตรงกันกับฝั่ง Flutter)
val channel = MethodChannel(registrar.messenger(), "toast")
และทำการกำหนด methodCallhandler
ที่รับ Parameter เป็น instance ที่มี interface MethodCallHandler
ซึ่งก็คือตัวมันเอง (เราจะเขียนแยกเป็น Class อื่นก็ได้นะ)
channel.setMethodCallHandler(ToastPlugin())
และเมื่อเรา implements interface MethodCallHandler
แล้วเราจะต้อง override onMethodCall
ด้วย
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
แก้โค้ดให้เรียกใช้ Toast ได้ตามนี้
package me.intception.toast
import android.widget.Toast // เพิ่ม Lib สำหรับเรียก Toast
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
import io.flutter.plugin.common.PluginRegistry.Registrar
class ToastPlugin(var registrar: Registrar): MethodCallHandler { // เพิ่มให้ ToastPlugin รับค่า registrar ใน constructor
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "toast")
channel.setMethodCallHandler(ToastPlugin(registrar)) // สร้าง ToastPlugin พร้อมส่ง registrar
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "callAwesomeToast") {
Toast.makeText(this.registrar.context(), "Its toast!", Toast.LENGTH_LONG).show()
result.success(null)
} else {
result.notImplemented()
}
}
}
ลองแก้ไฟล์ example/lib/main.dart
ใหม่เป็น
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:toast/toast.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<void> callMyToast() async {
await Toast.toast();
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: RaisedButton(
onPressed: callMyToast,
child: Text('Toast'),
),
),
),
);
}
}
เมื่อรันแล้วลองกดปุ่มดูก็จะได้ตามภาพ
# แต่ๆๆ
แต่ยังไม่จบเพียงเท่านี้ เพราะ String ที่เราใส่มันยัง Hard Code อยู่เลย
ผมจึงแก้ lib/toast.dart
import 'dart:async';
import 'package:flutter/services.dart';
class Toast {
static const MethodChannel _channel = const MethodChannel('toast');
static Future<void> toast(String text) async {
_channel.invokeMethod('callAwesomeToast', text);
}
}
android/src/main/.../ToastPlugin.kt
package me.intception.toast
import android.widget.Toast
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
import io.flutter.plugin.common.PluginRegistry.Registrar
class ToastPlugin(var registrar: Registrar): MethodCallHandler {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "toast")
channel.setMethodCallHandler(ToastPlugin(registrar))
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "callAwesomeToast") {
var customText = call.arguments<String>()
Toast.makeText(this.registrar.context(), customText, Toast.LENGTH_LONG).show()
result.success(null)
} else {
result.notImplemented()
}
}
}
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:toast/toast.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<void> callMyToast() async {
await Toast.toast('This plugin is awesome!!');
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: RaisedButton(
onPressed: callMyToast,
child: Text('Toast'),
),
),
),
);
}
}
หลังจากนั้นลองรันใหม่
# เอา Plugin มาใช้งานจริง
ในโปรเจคที่เราต้องการใช้งานนั้น เราแค่เพิ่ม dependencies ในไฟล์ pubspec.yaml
ก็ได้แล้วครับ
dependencies:
toast:
path: ./path/to/plugin/root
ดาวน์โหลด Source Code ที่นี่ (opens new window)
# สรุป
อาจจะงงๆหน่อยนะครับ แต่ลองทำความเข้าใจดูไม่ยากอย่างที่คิด สู้ๆครับ (บอกตัวเอง 🤣)
ถ้าหากมีข้อสงสัยอะไรสามารถติดต่อผ่าน Facebook Page (opens new window) ของผมได้ครับ