เนื้อหาตอนต่อไปนี้จะมาดู 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(); } )
ผลลัพธ์ที่ได้
![](https://i.imgur.com/tbZR3nx.png)
กรณีประยุกต์กับ 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(); } )
ผลลัพธ์ที่ได้
![](https://i.imgur.com/cdYa0uM.png)
ตอนนี้เราได้รู้จักแนวทางการประยุกต์การสร้างลิสรายการในรูปแบบต่างๆ ให้สังเกตให้ค่า 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(), ); } }
ผลลัพธ์ที่ได้
![](https://media3.giphy.com/media/bzqaVKRuVNvr7hIxsB/giphy.gif?cid=790b761149132b8c7812260a85065191bddf9bd26caff61d&rid=giphy.gif&ct=g)
เนื้อหานี้จะเน้นไปที่การใช้งาน PopupMenuButton รายละเอียดโค้ดอื่นๆ ที่เสริมเข้ามามีรูปแบบ
การใช้งานเหมือนบทความตอนที่แล้ว คำอธิบายแสดงในโค้ด
หวังว่าเนื้อหานี้จะทำให้เราสามารถประยุกต์การใช้งาน PopupMenuButton เพื่อกำหนดคำสั่งเพิ่ม
เติมที่ต้องการได้ เนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม