# เขียน Native มาใช้บน Flutter

#Dev#Flutter#Plugin

บางครั้งเราอาจจะต้องเขียน Plugin ที่ยืมความสามารถของ Native ขึ้นมาใช้เองถ้าไม่มีคนเขียนไว้ให้ ซึ่งแน่นอนว่าการจะเขียนได้นั้นต้องพอมีความรู้ในการเขียน Native สักนิดสักหน่อย ไม่ว่าจะเป็นภาษา Kotlin (Android) หรือ Swift (iOS) ซึ่งผมพอเข้าใจ Kotlin บ้างเล็กน้อย...ส่วน Swift นี่ไม่รู้เรื่องเลย 🤣
ในบทความนี้จะสอนวิธีเขียนตั้งแต่ต้น และพยายามอธิบายให้เข้าใจเขียนเองได้ครับ

Header

# เตรียมความพร้อมสิ่งที่ควรมี

  1. Android Studio ที่ติดตั้ง Flutter Plugin แล้ว
  2. Visual Studio Code ที่ติดตั้ง Flutter Plugin แล้ว

# พื้นฐานของ Plugin ใน Flutter

หลักการแบบสั้นๆก็คือ เราจะทำการเปิดช่องให้ Flutter และ Native คุยกันผ่าน MethodChannel

Title
รูปจาก Flutter Official

# สร้างโปรเจค Flutter Plugin

เปิด Android Studio ขึ้นมาแล้วกดสร้างโปรเจค Flutter

Title
สร้างโปรเจค Flutter

เลือกสร้าง Flutter Plugin

Title

ในที่นี้ผมจะทำ Plugin เรียกใช้ Toast (เป็น Alert เล็กๆ...เพื่อใครไม่รู้)

Title
ชื่อโปรเจคก็ตั้งง่ายๆแบบนี้ล่ะ

ที่สำคัญคืออย่าลืมติ๊ก Kotlin เพราะเราจะเขียนด้วย Kotlin เสร็จแล้วก็ Finish ได้เลยครับ

Title

คลิกเลือกไฟล์ main.dart แล้วลองรันดู จะต้องได้โปรแกรมหน้าตาตามนี้

Title
Title

# เริ่มเขียน 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'),
          ),
        ),
      ),
    );
  }
}

เมื่อรันแล้วลองกดปุ่มดูก็จะได้ตามภาพ

Title

# แต่ๆๆ

แต่ยังไม่จบเพียงเท่านี้ เพราะ 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'),
          ),
        ),
      ),
    );
  }
}

หลังจากนั้นลองรันใหม่

Title

# เอา Plugin มาใช้งานจริง

ในโปรเจคที่เราต้องการใช้งานนั้น เราแค่เพิ่ม dependencies ในไฟล์ pubspec.yaml ก็ได้แล้วครับ

dependencies:
  toast:
    path: ./path/to/plugin/root

ดาวน์โหลด Source Code ที่นี่ (opens new window)

# สรุป

อาจจะงงๆหน่อยนะครับ แต่ลองทำความเข้าใจดูไม่ยากอย่างที่คิด สู้ๆครับ (บอกตัวเอง 🤣)
ถ้าหากมีข้อสงสัยอะไรสามารถติดต่อผ่าน Facebook Page (opens new window) ของผมได้ครับ

อัปเดตเมื่อ: 1 ปีที่แล้ว