การใช้งาน Dismissible Widget ร่วมกับ ListView ใน Flutter

บทความใหม่ ยังไม่ถึงปี โดย Ninenik Narkdee
dismissible listtile flutter listview alertdialog

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ dismissible listtile flutter listview alertdialog



เนื้อหานี้เรายังอยู่กับการจัดการไฟล์ และโฟลเดอร์
แต่จะมาเสริมการประยุกต์การใช้งาน Dismissible Widget
เข้ามาเล็กน้อย เป็นแนวทางไว้นำไปใช้  ซึ่งจากตอนที่แล้ว
เราสร้างลิสรายการไฟล์ต่างๆ ในโฟลเดอร์ปัจจุบันที่กำลัง
ใช้งานอยู่ในรูปแบบของ ListView  เราจะมาประยุกต์โดยการ
ใช้งาน Dismissible ให้สามารถเลือกทำการปัดรายการไฟล์หรือ
โฟลเดอร์ที่ต้องการ แล้วเลิกที่จะลบรายการนั้นๆ ได้ ทบทวนตอนที่
แล้วได้ที่บทความ
    การใช้งาน Path Provider และการเชียนอ่าน File ใน Flutter http://niik.in/1066 
 
  *เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/960
 
 

ตัวอย่างผลลัพธ์และการทำงาน

 


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

การใช้งาน 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, // ค่าเริ่มต้น แนวนอน
 
    ตัวอย่าง
 

 
 
 

    ไฟล์ home.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';

import '../components/sidemenu.dart';
  
class Home extends StatefulWidget {
  static const routeName = '/';

  const Home({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
      return _HomeState();
  }
}
  
class _HomeState extends State<Home> {

  List<FileSystemEntity?>? _folders;
  String _currentPath = '';  // เก็บ path ปัจจุบัน
  Directory? _currentFolder; // เก็บ โฟลเดอร์ที่กำลังใช้งาน

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _loadFolder();
  }

  // เปิดโฟลเดอร์ และแสดงรายการในโฟลเดอร์
  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 _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 _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!);
    });
  }

  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);
    });

  }     

  @override
  Widget build(BuildContext context) {

      return Scaffold(
          appBar: AppBar(
              title: Text('Home'),
              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.flutter_app3', '/')}'),
                        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')), // กรณีไม่มีรายการ
                      ),
                  ],
          ),
          drawer: SideMenu(),
      );
  }
}
 
    เราเพิ่มส่วนของคำสั่งลบไฟล์ และลบโฟลเดอร์เพิ่มเข้ามา
 
// คำสังลบไฟล์
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 ทำให้ไม่มีค่าถูกส่งไปใช้งาน ดังนั้น ก็ให้ไปทำคำสั่ง
ลบก่อน แล้วค่อยดึงออกจากลิสรายการ ตามลำดับ
 
    ดูตัวอย่างการทำงานอีกครั้ง
 
 


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


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ









เนื้อหาที่เกี่ยวข้อง









URL สำหรับอ้างอิง











เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ