เนื้อหานี้เรายังอยู่กับการจัดการไฟล์ และโฟลเดอร์
แต่จะมาเสริมการประยุกต์การใช้งาน Dismissible Widget
เข้ามาเล็กน้อย เป็นแนวทางไว้นำไปใช้ ซึ่งจากตอนที่แล้ว
เราสร้างลิสรายการไฟล์ต่างๆ ในโฟลเดอร์ปัจจุบันที่กำลัง
ใช้งานอยู่ในรูปแบบของ ListView เราจะมาประยุกต์โดยการ
ใช้งาน Dismissible ให้สามารถเลือกทำการปัดรายการไฟล์หรือ
โฟลเดอร์ที่ต้องการ แล้วเลิกที่จะลบรายการนั้นๆ ได้ ทบทวนตอนที่
แล้วได้ที่บทความ
การใช้งาน Path Provider และการเชียนอ่าน File ใน Flutter http://niik.in/1066
https://www.ninenik.com/content.php?arti_id=1066 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/1066
ตัวอย่างผลลัพธ์และการทำงาน
เราทำการปัดรายการไฟล์หรือโฟลเดอร์ไปทางซ้าย หรือขวา ก็จะมีคำสั่งที่รองรับการทำงาน
ในตัวอย่างเราปัดไปซ้ายเพื่อลบ ก็จะขึ้นแจ้งให้ยืนยันการลบ กรณีที่เราต้องการป้องกันการปัดรายการ
โดยไม่ตั้งใจ เมื่อเรายืนยันการลบแล้ว รายการก็จะถูกลบออกจากลิส และทำคำสั่งลบข้อมูลต่อไป
การใช้งาน Dismissible Widget
Dismissible Widget เป็น widget ที่สามารถนำออกได้โดยการปัดไปตามทิศทางที่กำหนด โดย child
ที่กำหนดใน widget นี้จะเลื่อนออกไปจากหน้าจอ แล้วเกิด event ที่ชื่อ onDismissed ขึ้น นั่นคือเมื่อ
รายการถูกลบออกไป เราก็สามารถกำหนดคำสั่งการทำงานในให้กับ event นี้ได้
รูปแบบการใช้งาน
return Dismissible( key: UniqueKey(), confirmDismiss: (direction) async { }, onDismissed: (direction) { // ปัดไปทางขวา - บนลงล่าง if(direction == DismissDirection.startToEnd){ } // ปัดไปซ้าย - ล่างขึ้นบน if(direction == DismissDirection.endToStart){ } }, background: Container( // พื้นหลังปัดไปทางขวา color: Colors.green, ), secondaryBackground: Container( // พื้นหลังปัดไปทางซ้าย color: Colors.red, ), child: Text('Dismiss widget') );
รูปแบบการใช้งานข้างต้น รองรับการกำหนด รองรับการยืนยันการทำรายการ หรือก่อนเกิด onDismissed
event ขึ้น ถ้ายกเลิกก็จะไม่เกิด event child ก็จะไม่ถูกลบออกจากรายการ ถ้ากำหนดสีพื้นหลังแค่ค่า
background ค่าพื้นหลังทั้งปัดซ้ายหรือขวาก็จะเป็นค่าสีเดียวกัน เราสามารถกำหนดให้ทำงานเมื่อปัดขวา
อย่าเดียว หรือปัดซ้ายอย่างเดียว หรือทั้งปัดขวาและปัดซ้าย ได้โดยใช้ค่า direction โดยกำหนดเงื่อนไข
ให้ทำงานเฉพาะที่เป็นค่าตามต้องการ
onDismissed: (direction) { // ปัดไปทางขวา - บนลงล่าง if(direction == DismissDirection.startToEnd){ } // ปัดไปซ้าย - ล่างขึ้นบน if(direction == DismissDirection.endToStart){ } },
กรณีรองรับการปัดทุกทิศทาง ก็ไม่ต้องกำหนดเงื่อนไขเข้าไป
onDismissed: (direction) { // ทำงานทุกเงื่อนไขทิศทางที่ปัด },
กรณีใช้กับรายการที่มีหลายๆ รายการเช่น ListView ควรกำหนด่า key เข้าไปเพื่อเป็นการบอกว่าเป็น key
หรือ ไอดีอะไร จะได้เป็นแยกแต่ละรายการชัดเจนไม่ซ้ำกัน และสามารถอ้างอิงได้ ในตัวอย่างเราสามารถใช้
ค่า Key เป็น key: UniqueKey() ได้
เราสามารถกำหนดให้รองรับการยืนยันก่อน ทำงานได้ โดยกำหนดในส่วนของ confirmDismiss
ในรูปแบบดังนี้
confirmDismiss: (direction) async { return await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text("Confirm"), content: const Text("Are you sure you wish to delete this item?"), actions: <Widget>[ ElevatedButton( onPressed: () => Navigator.of(context).pop(true), child: const Text("DELETE") ), ElevatedButton( onPressed: () => Navigator.of(context).pop(false), child: const Text("CANCEL"), ), ], ); }, ); },
เราใช้คำสั่ง showDialog() แล้วเรียกใช้งาน AlertDialog widget โดยจะคืนค่า true กลับมาก็ต่อเมื่อ
กดที่ปุ่ม DELETE หรือยืนยันการลบเท่านั้น ส่วนเงื่อนไขอื่นๆ เป็นการยกเลิก
ตัวอย่าง
ในส่วนของการกำหนดสีพื้นหลัง เราสามารถกำหนดรูปแบบการแสดงด้านใน เพื่อให้ผู้ใช้เข้าใจถึงการ
ทำงานได้ว่า หากปัดไปด้านนี้จะทำงานยังไง เช่น เราต้องการแสดงรูปถังขยะ กรณีปัดไปทางซ้าย ก็สามารถ
ทำได้ดังนี้
secondaryBackground: Container( color: Colors.red, child: Align( alignment: Alignment.centerRight, child: Padding( padding: EdgeInsets.symmetric(horizontal: 10.0), child: FaIcon(FontAwesomeIcons.trashAlt), ) ) ),
ตัวอย่าง
โดยค่าเริ่มต้น ระยะทางการปัดเลื่อนรายการที่จะมีผลให้เกิด onDismissed ขึ้นจะอยู่ที่ 0.4 หรือประมาณ
40 เปอร์เซ็นต์ของความกว้างรายการ นั่นหมายความว่า ถ้าเราปัดไปได้ยังไม่ถึง 40% รายการก็จะปัดกลับ และ
ยกเลิกการ dismiss ไป แต่ถ้าเราปัดไปได้ที่ 40% ขึ้นไป รายการก็จะทำการ dismiss ค่าส่วนนี้ เราสามารถ
เปลี่ยนแปลงได้ ผ่านค่า dismissThresholds สมมติเช่น เรามองว่า 0.4 น้ยยไป กลัวว่าคนอาจจะเผลอไปโดน
โดยไม่รู้ตัว และเราไม่ได้ตั้งให้ confirm เพราะต้องการให้ทำงานรวดเร็วลดขั้นตอน เราก็อาจจะเพิ่มระยะเป็นสัก
60% ก็สามารถกำหนดได้ดังนี้
// ถ้ากำหนดค่าเป็น 1.0 จะหมายถึงต้องปัดให้สุดขอบของรายการ ถึงจะทำงาน dismissThresholds: const { DismissDirection.endToStart:0.6, DismissDirection.startToEnd:0.6},
หากต้องการเปลี่ยนทิศทางการปัดจากแนวนอน เป็นนแวตั้ง ก็สามารถใช้ค่า direction กำหนดทิศทางได้
direction: DismissDirection.vertical, // แนวตั้ง // direction: DismissDirection.horizontal, // ค่าเริ่มต้น แนวนอน
ตัวอย่าง
ไฟล์ explorer.dart
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:path_provider/path_provider.dart'; class Explorer extends StatefulWidget { static const routeName = '/explorer'; const Explorer({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ExplorerState(); } } class _ExplorerState extends State<Explorer> { List<FileSystemEntity?>? _folders; String _currentPath = ''; // เก็บ path ปัจจุบัน Directory? _currentFolder; // เก็บ โฟลเดอร์ที่กำลังใช้งาน @override void initState() { // TODO: implement initState super.initState(); _loadFolder(); } void _loadFolder() async { // ข้อมูลเกี่ยวกับโฟลเดอร์ Directory ต่างๆ final tempDirectory = await getTemporaryDirectory(); final appSupportDirectory = await getApplicationSupportDirectory(); final appDocumentsDirectory = await getApplicationDocumentsDirectory(); final externalDocumentsDirectory = await getExternalStorageDirectory(); final externalStorageDirectories = await getExternalStorageDirectories(type: StorageDirectory.music); final externalCacheDirectories = await getExternalCacheDirectories(); print(tempDirectory); print(appSupportDirectory); print(appDocumentsDirectory); print(externalDocumentsDirectory); print(externalCacheDirectories); print(externalStorageDirectories); // เมื่อโหลดขึ้นมา เาจะเปิดโฟลเดอร์ของ package เป้นโฟลเดอร์หลัก _currentFolder = appDocumentsDirectory.parent; _currentPath = appDocumentsDirectory.parent.path; final myDir = Directory(_currentPath); setState(() { _folders = myDir.listSync(recursive: false, followLinks: false); }); } // คำสังลบไฟล์ void _deleteFile(path) async { final deletefile = File(path); // กำหนด file object final isExits = await deletefile.exists(); // เช็คว่ามีไฟล์หรือไม่ if(isExits){ // ถ้ามีไฟล์ try{ await deletefile.delete(); }catch(e){ print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } // คำสั่งลบโฟลเดอร์ void _deleteFolder(path) async { final deleteFolder = Directory(path); // สร้าง directory object var isExits = await deleteFolder.exists(); // เช็คว่ามีแล้วหรือไม่ if(isExits){ // ถ้ามีโฟลเดอร์ try{ await deleteFolder.delete(recursive: true); }catch(e){ print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } // เปิดโฟลเดอร์ และแสดงรายการในโฟลเดอร์ void _setPath(dir) async { _currentFolder = dir; _currentPath = dir.path; final myDir = Directory(_currentPath); try{ setState(() { _folders = myDir.listSync(recursive: false, followLinks: false); }); }catch(e){ print(e); } } // จำลองสร้างไฟล์ใหม่ void _newFile() async { String newFile = "${_currentFolder!.path}/myfile.txt"; final myfile = File(newFile); // กำหนด file object final isExits = await myfile.exists(); // เช็คว่ามีไฟล์หรือไม่ if(!isExits){ // ถ้ายังไม่มีไฟล์ try{ // สร้างไฟล์ text var file = await myfile.writeAsString( 'Hello World' ); print(file); }catch(e){ print(e); } }else{ // ถ้ามีไฟล์อยู่แล้ว จำลองการลบข้อมูล try{ await myfile.delete(); }catch(e){ print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } // คำสั่งจำลองการสร้างโฟลเดอร์ void _newFolder() async { String newFolder = "${_currentFolder!.path}/mydir"; final myDir = Directory(newFolder); // สร้าง directory object var isExits = await myDir.exists(); // เช็คว่ามีแล้วหรือไม่ if(!isExits){ // ถ้ายังไม่มีสร้างโฟลเดอร์ขึ้นมาใหม่ try{ var directory = await Directory(newFolder).create(recursive: true); print(directory); }catch(e){ print(e); } }else{ // ถ้ามีแล้วจำลองการทำคำสั่งลบ try{ await myDir.delete(recursive: true); }catch(e){ print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Explorer'), actions: <Widget>[ // IconButton( onPressed: _newFolder, // สร้างโฟลเดอร์ใหม่ icon: FaIcon(FontAwesomeIcons.folderPlus), ), IconButton( onPressed: _newFile, // สร้างไฟล์ใหม่ icon: FaIcon(FontAwesomeIcons.fileAlt), ), ], ), body: Column( mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ ListTile( leading: FaIcon(FontAwesomeIcons.angleLeft), title: Text('${_currentPath.replaceAll('/data/user/0/com.example.demo_app', '/')}'), onTap: (){ _setPath(_currentFolder!.parent); } ), Expanded( child: _folders!=null // เมื่อไม่ใช่ค่า null ? ListView.separated( // กรณีมีรายการ แสดงปกติ itemCount: _folders==null ? 0 : _folders!.length, itemBuilder: (context, index) { var isFolder = _folders![index] is Directory ? true : false; // เช็คว่าเป็นโฟลเดอร์ var isFile = _folders![index] is File ? true : false; // เช็คว่าเป็นไฟล์ if(_folders![index] != null){ // เอาเฉพาะชื่อหลัง / ตัวสุดท้าย String fileName = _folders![index]!.path.split('/').last; return Dismissible( key: UniqueKey(), confirmDismiss: (direction) async { return await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text("Confirm"), content: const Text("Are you sure you wish to delete this item?"), actions: <Widget>[ ElevatedButton( onPressed: () => Navigator.of(context).pop(true), child: const Text("DELETE") ), ElevatedButton( onPressed: () => Navigator.of(context).pop(false), child: const Text("CANCEL"), ), ], ); }, ); }, onDismissed: (direction) { // ปัดไปทางขวา - บนลงล่าง if(direction == DismissDirection.startToEnd){ } // ปัดไปซ้าย - ล่างขึ้นบน if(direction == DismissDirection.endToStart){ try{ setState(() { if(isFile){ // ถ้าเป็นไฟล์ ส่ง path ไฟล์ไปลบ _deleteFile(_folders![index]!.path); } if(isFolder){ // ถ้าเป็นโฟลเดอร์ส่ง path โฟลเดอร์ไปลบ _deleteFolder(_folders![index]!.path); } // ต้องลบข้อมูลก่อน แล้วค่อยลบรายการในลิส _folders!.removeAt(index); }); }catch(e){ print(e); } } ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('$index dismissed'))); }, background: Container( color: Colors.green, ), secondaryBackground: Container( color: Colors.red, child: Align( alignment: Alignment.centerRight, child: Padding( padding: EdgeInsets.symmetric(horizontal: 10.0), child: FaIcon(FontAwesomeIcons.trashAlt), ) ) ), child: ListTile( leading: isFolder ? FaIcon(FontAwesomeIcons.solidFolder) : FaIcon(FontAwesomeIcons.file), title: Text('${fileName}'), onTap: (isFolder==true) ? (){ // กรณีเป้นโฟลเดอร์ _setPath(_folders![index]!); // ถ้ากด ให้ทำคำสั่งเปิดโฟลเดอร์ } : (){}, // กรณีเป็นไฟล์ ) ); }else{ return Container(); } }, separatorBuilder: (BuildContext context, int index) => const Divider(height: 1,), ) : const Center(child: Text('No items')), // กรณีไม่มีรายการ ), ], ), ); } }
เราเพิ่มส่วนของคำสั่งลบไฟล์ และลบโฟลเดอร์เพิ่มเข้ามา
// คำสังลบไฟล์ void _deleteFile(path) async { final deletefile = File(path); // กำหนด file object final isExits = await deletefile.exists(); // เช็คว่ามีไฟล์หรือไม่ if(isExits){ // ถ้ามีไฟล์ try{ await deletefile.delete(); }catch(e){ print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } // คำสั่งลบโฟลเดอร์ void _deleteFolder(path) async { final deleteFolder = Directory(path); // สร้าง directory object var isExits = await deleteFolder.exists(); // เช็คว่ามีแล้วหรือไม่ if(isExits){ // ถ้ามีโฟลเดอร์ try{ await deleteFolder.delete(recursive: true); }catch(e){ print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); }
จากนั้นก็ใช้งาน Dismissible โดยให้ ListTile เป็น child ในตัวอย่างเรากำหนดคำสั่งการทำงานเฉพาะ
ส่วนของปัดไปทางซ้าย หรือการลบข้อมูล
// ปัดไปซ้าย - ล่างขึ้นบน if(direction == DismissDirection.endToStart){ try{ setState(() { if(isFile){ // ถ้าเป็นไฟล์ ส่ง path ไฟล์ไปลบ _deleteFile(_folders![index]!.path); } if(isFolder){ // ถ้าเป็นโฟลเดอร์ส่ง path โฟลเดอร์ไปลบ _deleteFolder(_folders![index]!.path); } // ต้องลบข้อมูลก่อน แล้วค่อยลบรายการในลิส _folders!.removeAt(index); }); }catch(e){ print(e); } }
สังเกตว่า เราต้องทำการลบข้อมูลก่อน ที่จะลบรายการออกจากลิส เพราะถ้าเราไปเผลอลบลิสก่อน
ข้อมูลในลิสที่จะส่งไปลบ จะถูกดึงออกจาก view ทำให้ไม่มีค่าถูกส่งไปใช้งาน ดังนั้น ก็ให้ไปทำคำสั่ง
ลบก่อน แล้วค่อยดึงออกจากลิสรายการ ตามลำดับ
ดูตัวอย่างการทำงานอีกครั้ง
หวังว่าจะเป็นแนวทางนำไปปรับใช้งานต่อไป เนื้อหาเกี่ยวกับไฟล์ และการใช้งานหรือการประยุกต์
โดยอาศัยเนื้อหานี้ ยังมีอยู่รอติดตามว่าจะเป็นอะไรในบทความต่อๆ ไป