เนื้อหาตอนต่อไปนี้จะมาดู Widget เล็กๆ ที่มีรูปแบบการใช้งาน
ง่ายๆ ที่เรียกว่า PopupMenuButton เป็นปุ่มเมนูเพิ่มเติมที่แสดงมา
ให้เราเลือกใช้งาน หรือกำหนดการทำคำสั่งที่ต้องการทำงานเพิ่มเติม
จะใช้เนื้อหาจากตอนที่แล้ว จะจัดการเฉพาะในไฟล์ article.dart
ทบทวนตอนที่แล้วได้ที่บทความ
การใช้งาน WebView แสดงเว็บไซต์ ใน Flutter http://niik.in/1043
https://www.ninenik.com/content.php?arti_id=1043 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/961
การใช้งาน PopupMenuButton
ตัว PopupMenuButton เมื่อกำหนดหรือเรียกใช้งาน จะแสดงเป็นปุ่มไอคอน จุด 3 จุดใน
แนวตั้งหรือชื่อไอคอนว่า Icons.more_vert เป็นการสื่อว่ามีเพิ่มเติม เมื่อเรากดที่ปุ่มนี้ก็จะแสดง
ลิสราายการปุ่มต่างๆ ให้เราเลือก ถ้าเราเลือกปุ่มรายการใดๆ ก็จะใช้ค่าปุ่มรายการนั้นๆ เป็นตัว
กำหนดเงื่อนไขการทำงานอีกที ถ้าเราไม่ต้องการเลือกรายการที่แสดง ก็สามารถกดไปที่พื้นที่ว่าง
นอกรายการเพื่อปิดปุ่มนั้นๆ ไป ในการสร้างปุ่ม PopupMenuButton จะต้องมีการกำหนด itemBuilder
เพื่อสร้างรายการของปุ่ม
รูปแบบการใช้งาน PopupMenuButton
PopupMenuButton<T>(
onSelected: (T result) { },
itemBuilder: (BuildContext context) => <PopupMenuEntry<T>>[
const PopupMenuItem<T>(
value: T.value,
child: Text('Menu 1'),
),
const PopupMenuItem<T>(
value: T.value,
child: Text('Menu 2'),
),
],
)
สัญลักษณ์ T คือข้อมูลประเภท Type หรือก็คือ class ดูตัวอย่าง type ในภาษา Dart
// ข้อมูล type ColorOption
enum ColorOption { red, green, blue }
// ข้อมูล type Option
class Option{
int a = 0;
}
ทั้ง ColorOption และ Option เป็นรูปแบบหนึ่งของ class โดยตัว ColorOption จะใช้คำว่า enum เป็นคำ
keyword เป็น class พิเศษเฉพาะที่กำหนดจำนวนของค่าคงที่ ที่เรียกว่า enum type ข้อมูลที่มีการระบุแจกแจง
ค่าไว้อย่างชัดเจน ค่าของ Enum จะอ้างอิงผ่าน property ที่ชื่อ values
print(ColorOption.values); // แสดงข้อมูลของ enum type
ก็จะได้เป็น List<ColorOption> มีค่าเป็น
[ColorOption.red, ColorOption.green, ColorOption.blue] // ColorOption.values[0] = ColorOption.red // ColorOption.values[1] = ColorOption.green // ColorOption.values[2] = ColorOption.blue
เราจะใช้ช้อมูล Enum type สำหรับกำหนดรายการให้กับ PopupMenuButton ยกตัวอย่างเช่นข้อมูล
enum ColorOption { red, green, blue }
สามารถกำหนดใช้งานใน PopupMenuButton เป็นดังนี้
PopupMenuButton<ColorOption>(
onSelected: (ColorOption result) { },
itemBuilder: (BuildContext context) => <PopupMenuEntry<ColorOption>>[
const PopupMenuItem<ColorOption>(
value: ColorOption.red,
child: Text('Menu 1 Red'),
),
const PopupMenuItem<ColorOption>(
value: ColorOption.green,
child: Text('Menu 2 Green'),
),
const PopupMenuItem<ColorOption>(
value: ColorOption.blue,
child: Text('Menu 3 Blue'),
),
],
)
หรือกรณีเราใช้เป็นข้อมูล String type ก็จะเป็นประมาณนี้
PopupMenuButton<String>(
onSelected: (String result) { },
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: '1',
child: Text('Menu 1 Red'),
),
const PopupMenuItem<String>(
value: '2',
child: Text('Menu 2 Green'),
),
const PopupMenuItem<String>(
value: '3',
child: Text('Menu 3 Blue'),
),
],
)
หรือกรณีเราใช้เป็นข้อมูล boolean type ก็จะเป็นประมาณนี้
PopupMenuButton<bool>(
onSelected: (bool result) { },
itemBuilder: (BuildContext context) => <PopupMenuEntry<bool>>[
const PopupMenuItem<bool>(
value: true,
child: Text('Menu 1 Red'),
),
const PopupMenuItem<bool>(
value: false,
child: Text('Menu 2 Green'),
),
],
)
เราสามารถสร้างลิสรายการจากข้อมูลอาเรย์หรือ List ได้ง่ายเพื่อลดขึ้นตอนการกำหนดแต่ละรายการ
// สร้างตัวแปร ลืสรายการเมนูที่เป็น String var myMenuItems = <String>[ 'Home', 'Profile', 'Setting', ];
จากนั้นทำการวนลูปแสดงใน PopupMenuItem ดังนี้
PopupMenuButton<String>(
onSelected: (String result) { },
itemBuilder: (BuildContext context) {
return myMenuItems.map((String choice) {
return PopupMenuItem<String>(
child: Text(choice),
value: choice,
);
}).toList();
}
)
ผลลัพธ์ที่ได้

กรณีประยุกต์กับ Map Type เพิ่ม FontAwesome ไอคอนเข้าไป
// สร้างตัวแปร ลืสรายการเมนูที่เป็น Map<dynamic, dynamic>
var myMenuItems = <Map>[
{'icon':FontAwesomeIcons.home,'value':'home','label':'Home'},
{'icon':FontAwesomeIcons.userAlt,'value':'profile','label':'Profile'},
{'icon':FontAwesomeIcons.cog,'value':'setting','label':'Setting'}
];
จากนั้นทำการวนลูปแสดงใน PopupMenuItem ดังนี้
PopupMenuButton<Map>(
onSelected: (Map result) { },
itemBuilder: (BuildContext context) {
return myMenuItems.map((Map choice) {
return PopupMenuItem<Map>(
child: ListTile(
leading: Icon(choice['icon']),
title: Text(choice['label'], style: Theme.of(context).textTheme.bodyText1),
),
value: choice,
);
}).toList();
}
)
ผลลัพธ์ที่ได้

ตอนนี้เราได้รู้จักแนวทางการประยุกต์การสร้างลิสรายการในรูปแบบต่างๆ ให้สังเกตให้ค่า value ของ
PopupMenuItem คือเมื่อเราแตะเลือกที่รายการใด ค่า value นี้จะถูกส่งเข้าไปใน callback ฟังก์ชั่นของ
onSelected ดังนั้นในการกำหนดเงื่อนไขการทำงาน ก็จะไปกำหนดในค่าที่เลือกว่าเป็นค่าใด และให้ทำงาน
อย่างเรา ยกตัวอย่างรูปแบบกรณีล่าสุด ก็จะเป็น
PopupMenuButton<Map>(
onSelected: (Map result) {
result = Map<String, dynamic>.from(result); // แปลงค่ากลับ
switch (result['value']) { // ตรวจสอบค่าที่จะใช้งานเป็นเงื่อนไข
case 'home':
print('Home clicked');
break;
case 'profile':
print('Profile clicked');
break;
case 'setting':
print('Setting clicked');
break;
}
},
itemBuilder: (BuildContext context) {
return myMenuItems.map((Map choice) {
return PopupMenuItem<Map>(
child: ListTile(
leading: Icon(choice['icon']),
title: Text(choice['label'], style: Theme.of(context).textTheme.bodyText1),
),
value: choice,
);
}).toList();
}
)
เนื่องจากค่าจาก Map type เป็นข้อมูลที่มีความซับซ้อนดังนั้น จึงมีการแปลงกลับมาในรูปแบบที่สามารถ
อ้างอิงการใช้งานได้ก่อน แต่ถ้าเป็นค่าอื่นๆ เช่น boolean Sring Int Enum เหล่านี้ สามารถนำค่าไปตรวจ
สอบเป็นเงื่อนไขได้เลย
ในตัวอย่างการทำคำสั่งเมื่อเข้าเงื่อนไข จะใช้เป็นการเรียกใช้ฟังก์ชั่นอีกที เพราะถ้าเขียนการทำงานในนี้
ก็จะยาวเกินไป ข้างต้นเราแค่ทดสอบแสดงข้อความเท่านั้น
ไฟล์ article.dart แบบเต็ม
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class Articles extends StatefulWidget {
static const routeName = '/articles';
const Articles({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _ArticlesState();
}
}
class _ArticlesState extends State<Articles> {
// แก้ไขตัวแปรสำหรับ contrller ใหม่ ให้เป็นชนิดข้อมูล late
late final WebViewController _controller;
/*
ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier
ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง
ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management
library ที่ซับซ้อน เช่น Provider หรือ Bloc
*/
// กำหนดค่าเริ่มต้นเป็น false
final ValueNotifier<bool> _canGoBack = ValueNotifier<bool>(false);
final ValueNotifier<bool> _canGoForward = ValueNotifier<bool>(false);
// ส่วนของตัวแปรจัดการ cookies
final WebViewCookieManager _cookieManager = WebViewCookieManager();
// ส่วนของตัวแปร กำหนดให้ตรวจสอบว่าโหลด url แล้วหรือไม่เพื่อเรียกใช้งานเพียงครั้งเดียวที่เปิดขึ้นมา
bool _isUrlLoaded = false;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
print("WebView is loading (progress : $progress%)");
// Update loading bar.
},
onPageStarted: (String url) async {
_canGoBack.value = await _controller.canGoBack();
_canGoForward.value = await _controller.canGoForward();
},
onPageFinished: (String url) async {
_canGoBack.value = await _controller.canGoBack();
_canGoForward.value = await _controller.canGoForward();
},
onHttpError: (HttpResponseError error) {},
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) {// กำหนดการทำงานเมื่อคลิกลิ้งค์ในเว็บเพจ
// เช่นการตรวจ url และ block ไม่ให้ใช้้งาน url ที่กำหนด
if (request.url.startsWith('https://www.youtube.com/')) {
print('blocking navigation to $request}');
return NavigationDecision.prevent; // ถ้าเป็นจากลิ้งค์ youtube ให้ block
}
print('allowing navigation to $request');
return NavigationDecision.navigate; // ถ้าเป็นลิ้งค์อื่นๆ เข้าไปปกติ
},
),
// เพิ่มส่วนนี้เพื่อ สร้าง JavascriptChannel สำหรับรับค่าข้อมูลที่ส่งผ่านทาง JavaScript
)..addJavaScriptChannel(
'Toaster', // กำหนดชื่อสำหรับเรียกใช้งาน
onMessageReceived: (JavaScriptMessage message) {
print(message.message);
// ในที่นี้เมื่อได้ค่ามาแล้ว จะแสดงข้อความด้วย SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
},
);
}
@override
Widget build(BuildContext context) {
// เนื่องจาการใช้งาน PopupMenuButton จะมีการ rebuild widget ทุกครั้งที่กด
// ดังนั้นเพื่อไม่ให้มีการโหลดหน้าเพจ เมื่อกดที่ปุ่มเมนูนี้ เราต้องกำหนดเงื่อนไขว่า
// โหลดหน้าเพจเฉพาะครั้งแรกเท่าานั้น
if (!_isUrlLoaded) {
// รับค่า url ที่ส่งมาใน arguments
final url = ModalRoute.of(context)!.settings.arguments as String;
_controller.loadRequest(Uri.parse(url));
_isUrlLoaded = true;
}
return Scaffold(
appBar: AppBar(
title: Text('Articles'),
actions: <Widget>[
NavigationControls( // เมนูส่วนของการใช้งาน NavigationControls
controller: _controller,
canGoBack: _canGoBack,
canGoForward:
_canGoForward
),
SampleMenu( // เมนูส่วนของการใชงาน PopupMenuButton
controller: _controller,
cookieManager: _cookieManager),
],
),
body: WebViewWidget(controller: _controller),
floatingActionButton: scrollTopButton(), // เรียกใช้ปุ่มจากฟังก์ชั่น
);
}
// สร้างฟังก์ชั่น คืนค่าเป็น widget
Widget scrollTopButton() {
return FloatingActionButton( // คืนค่าเป็นปุ่มรูปหัวใจ
onPressed: () async { // ถ้ากด
// เรียกคำสั่ง javascript เลื่อน scroll ไปด้านบนสุด
await _controller.runJavaScript('window.scrollTo(0, 0);');
},
child: const Icon(Icons.arrow_upward),
);
}
}
// สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช
class NavigationControls extends StatelessWidget {
// กำหนด class constructor รับค่าที่จำเป็น
const NavigationControls({
required this.controller,
required this.canGoBack,
required this.canGoForward,
Key? key,
}) : super(key: key);
// กำหนดตัวแปรที่เกี่ยวข้อง
/*
ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier
ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง
ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management
library ที่ซับซ้อน เช่น Provider หรือ Bloc
*/
final WebViewController controller;
final ValueNotifier<bool> canGoBack;
final ValueNotifier<bool> canGoForward;
/*
ValueListenableBuilder เป็น widget ที่ใช้ในการสร้าง UI ที่ฟังการเปลี่ยนแปลงค่าของ
ValueNotifier และทำการ rebuild UI เมื่อค่าของ ValueNotifier มีการเปลี่ยนแปลง
*/
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
ValueListenableBuilder<bool>(
valueListenable: canGoBack,
builder: (context, value, child) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: value ? () => controller.goBack() : null,
);
},
),
ValueListenableBuilder<bool>(
valueListenable: canGoForward,
builder: (context, value, child) {
return IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: value ? () => controller.goForward() : null,
);
},
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => controller.reload(),
),
],
);
}
}
// กำหนด Enum Type สำหรับเป็นลิสรายการของ PopupMenuButton
enum MenuOptions {
showUserAgent,
listCookies,
clearCookies,
addToCache,
listCache,
clearCache,
}
// สร้าง widget สำหรับทำปุ่มควบคุม เพิ่มเติมแบบ PopupMenuButton
class SampleMenu extends StatelessWidget {
// กำหนด class constructor รับค่าที่จำเป็น
SampleMenu({
required this.controller,
required this.cookieManager,
Key? key,
}) : super(key: key);
final WebViewController controller; // ใช้งาน WebViewController
final WebViewCookieManager cookieManager; // ใช้งาน CookieManager
@override
Widget build(BuildContext context) {
return PopupMenuButton<MenuOptions>(
onSelected: (MenuOptions value) {
switch (value) { // ใช้เงื่อนไขค่าที่เลือก ทำฟังก์ชั่นที่ต้องการ
case MenuOptions.showUserAgent:
_onShowUserAgent(controller, context);
break;
case MenuOptions.listCookies:
_onListCookies(controller, context);
break;
case MenuOptions.clearCookies:
_onClearCookies(context);
break;
case MenuOptions.addToCache:
_onAddToCache(controller, context);
break;
case MenuOptions.listCache:
_onListCache(controller, context);
break;
case MenuOptions.clearCache:
_onClearCache(controller, context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
PopupMenuItem<MenuOptions>(
value: MenuOptions.showUserAgent,
child: const Text('Show user agent'),
// enabled: controller.!,
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.listCookies,
child: Text('List cookies'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.clearCookies,
child: Text('Clear cookies'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.addToCache,
child: Text('Add to cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.listCache,
child: Text('List cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.clearCache,
child: Text('Clear cache'),
),
],
);
}
// ส่วนของฟังก์ชั่นการทำงานต่างๆ
// ฟังก์ชั่นแสดง UserAgent ของ WebView
void _onShowUserAgent(
WebViewController controller, BuildContext context) async {
await controller.runJavaScript(
'Toaster.postMessage("User Agent: " + navigator.userAgent);');
}
// ฟังก์ชั่นแสดงรายการ cookie
void _onListCookies(
WebViewController controller, BuildContext context) async {
final String cookies = await controller
.runJavaScriptReturningResult('document.cookie')
.then((value) => value.toString());
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Cookies:'),
_getCookieList(cookies),
],
),
));
}
// ฟังก์ชั่นเพิ่มรายการ cache
void _onAddToCache(WebViewController controller, BuildContext context) async {
await controller.runJavaScript(
'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Added a test entry to cache.'),
));
}
// ฟังก์ชั่นแสดงรายการ cache
void _onListCache(WebViewController controller, BuildContext context) async {
await controller.runJavaScript('caches.keys()'
'.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
'.then((caches) => Toaster.postMessage(caches))');
}
// ฟังก์ชั่นล้างค่า cache
void _onClearCache(WebViewController controller, BuildContext context) async {
await controller.clearCache();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Cache cleared."),
));
}
// ฟังก์ชั่นล้างค่า cookie
void _onClearCookies(BuildContext context) async {
final bool hadCookies = await cookieManager.clearCookies();
String message = 'There were cookies. Now, they are gone!';
if (!hadCookies) {
message = 'There are no cookies.';
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message),
));
}
// ฟังก์ชั่นแสดงรายการ cookie
Widget _getCookieList(String cookies) {
if (cookies.isEmpty || cookies == '""') {
return Container();
}
final List<String> cookieList = cookies.split(';');
final Iterable<Text> cookieWidgets =
cookieList.map((String cookie) => Text(cookie));
return Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: cookieWidgets.toList(),
);
}
}
ผลลัพธ์ที่ได้

เนื้อหานี้จะเน้นไปที่การใช้งาน PopupMenuButton รายละเอียดโค้ดอื่นๆ ที่เสริมเข้ามามีรูปแบบ
การใช้งานเหมือนบทความตอนที่แล้ว คำอธิบายแสดงในโค้ด
หวังว่าเนื้อหานี้จะทำให้เราสามารถประยุกต์การใช้งาน PopupMenuButton เพื่อกำหนดคำสั่งเพิ่ม
เติมที่ต้องการได้ เนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม