ประยุกต์สร้าง TextEditor อย่างง่าย ใน Flutter

เขียนเมื่อ 2 ปีก่อน โดย Ninenik Narkdee
texteditor stream savefile readfile flutter

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

ดูแล้ว 3,991 ครั้ง


เนื้อหานี้จะมาดูต่อเกี่ยวกับการจัดการไฟล์ โดยเฉพาะ
ไฟล์ text นามสกุลไฟล์ txt เป็นตัวอย่างที่สามารถเอาไป
ปรับใช้งานได้ เช่น ต้องการให้ app มีส่วนของการจดบันทึก
ข้อมูลคล้ายๆ กับสมุดจดบันทึกที่เก็บข้อมูลไว้ในไฟล์ และ
สามารถเปิดขึ้นมาอ่านหรือแสดงได้ เนื้อหานี้ต่อเนื่องจากตอนที่
แล้ว ถึงจะไม่ได้สัมพันธ์กับเนื้อหาที่ผ่านมาแต่ก็ใช้โค้ดต่อเนื่อง
ทบทวนตอนที่แล้วได้ที่บทความ
    การทำ Selected Item ใน ListView เพื่อจัดการ ใน Flutter http://niik.in/1068
 
  *เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/960

 
 

ลำดับสิ่งที่เราจะทำ

    ในตัวอย่างที่ผ่านๆมา เรามีปุ่มสำหรับสร้างไฟล์และโฟลเดอร์ตรงมุมบนขวา ซึ่งกำหนดค่าแบบ
ตายตัวเป็นชื่อ myfile.txt กับโฟลเดอร์ชื่อ mydir ในที่นี้เราจะเปลี่ยนเฉพาะส่วนของการสร้างไฟล์
จากเดิมใช้ชื่อเป็น myfile.txt เราจะเปลี่ยนเป็นชื่อตัวเลข timestamp เพื่อให้ทุกครั้งที่กดสร้าง
ไฟล์ จะเป็นไฟล์ใหม่ เรื่อยๆ ไม่ซ้ำกัน  เปรียบเสมือนเราสร้างสมุดโน้ดขึ้นมา และพร้อมที่จะใส่ข้อ
มูลต่างๆ เข้าไปตามต้องการ  จากนั้นถ้าเรากดเปิดไฟล์ txt ที่เราสร้าง ก็จะทำการเปิดไฟล์นั้น พร้อม
ทั้งอ่านข้อมูลที่มีอยู่ภายในไฟล์ แล้วนำมาแสดงอีกหน้าหนึ่ง ที่มีฟอร์มและ TextFormField สำหรับ
แสดงข้อมูลที่เราสามารถแก้ไขได้ และเมื่อแก้ไข และบันทึกการแก้ไข ข้อมูลก็จะถูกเขียนทับไปที่
ไฟล์เดิมนั้น จัดเก็บเป็นข้อมูลไว้
    ดังนั้นสิ่งที่เราจะได้รู้และเข้าใจในบทความนี้ก็คือ การสร้างไฟล์ การอ่านไฟล์ การเขียนข้อมูลลงไฟล์
เป็นแนวทางไปปรับใช้งานในรูปแบบอื่นๆ ต่อไปได้
 
 

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

 


 
 
    เราสร้างไฟล์ text ขึ้นมาสามไฟล์ แล้วเปิดไฟล์ที่สองขึ้นมาแก้ไข เพิ่มข้อมูล แล้วกดบันทึก
 
 

เริ่มต้นการสร้าง TextEditor

    ในเนื้อหานี้นอกจากเราจะใช้งาน dart:io แล้ว ยังมีการใช้งาน dart:convert และ dart:async ร่วมด้วย
รายละเอียด package ที่ import มาใช้จะแสดงในหน้ารวมของโค้ดสุดท้ายในตอนท้ายของบทความ
 
    สิ่งแรกที่เราจะกำหนดเพิ่มเข้ามาคือฟอร์ม key และ ตัวแปรสำหรับ TextEditingController ใช้จัดการข้อมูล
ร่วมกับ TextFormField หรือส่วนของช่องสำหรับกรอกหรือแก้ไขข้อมูล
 
  // สร้างฟอร์ม key หรือ id ของฟอร์มสำหรับอ้างอิง
  final _formKey = GlobalKey<FormState>();    
  // กำหนดตัวแปรรับค่า
  final _textData = TextEditingController();     
 
    ตามด้วยกำหนดการยกเลิกการใช้งานกรณีปิดหน้านี้ไป เพื่อคืนค่าหน่วยความจำให้กับเครื่อง
 
  @override
  void dispose() {
    _textData.dispose(); 
    super.dispose();
  }
 
    เป็นสิ่งที่ควรทำเสมอเมื่อมีการใช้งาน controller ต่างๆ
 
    ต่อไปเราจะสร้างหน้า app สำหรับทำเป็น TextEditor รูปแบบที่ต้องการประมาณรูปด้านล่าง
 
 


 
 
    มีส่วนของ appbar ฝั่งซ้ายจะเป็นปุ่มปิด และฝั่งขวาจะเป็นปุ่มบันทึกข้อมูล ส่วนของ body จะเป็นส่วนของ
TextFormField ที่เป็น input รับข้อมูลที่เรากำหนดให้แสดงแบบเต็มพื้นที่ 
    การสร้างหน้า app เราสามารถสร้างเป็นอีกไฟล์ขึ้นมาได้ แต่ในที่นี้เราจะใช้วิธีการสร้างไว้ในฟังก์ชั่น โดย
คืนค่าเป็น Route<Object?> โดยใช้งาน DialogRoute แสดงในรุปแบบ dialog รูปแบบนี้เคยนำเสนอไปแล้ว
ในบทความ http://niik.in/1042  ในทุกๆ บทความผู้เขียนจะแทรกแนวทางการใช้งานต่างๆ ไว้ ให้สามารถ
กลับไปย้อนศึกษาและนำมาปรับประยุกต์ใช้งานต่อไปได้
    ฟังก์ชั่นด้านล่างจะคืนค่าเป็นเหมือนหน้า app ใหม่  เนื่องจากเป็น dialog เราใช้ Dismissible เพื่อให้สามารถ
ปัดลงเพื่อปิดได้ และเนื่องจากข้อมูลที่จะแสดงในหน้านี้ เป็นข้อมูลที่ต้องไปอ่านจากไฟล์ ซึ่งมีเวลาที่ต้องรอ
ข้อมูล เราจึงมีการใช้งาน FutureBuilder โดยดึงข้อมูลจากฟังก์ชั่น _readFile(file) นั่นคือเมื่อเราเปิดหน้านี้
เราจะไปทำการอ่านไฟล์แล้วนำข้อมูลมาแสดง
 
 
// สร้างฟังก์ชั่น ที่คืนค่าเป็น route ของ object ฟังก์ชั่นนี้ มี context และ product เป็น parameter
Route<Object?> _viewFile(BuildContext context, FileSystemEntity file) { 
  return DialogRoute<void>(
    context: context,
    builder: (context) {
      return Dismissible( // คืนค่าเป็น dismissible widget
        direction: DismissDirection.vertical, // เมื่อปัดลงในแนวตั้ง
        key: const Key('key'), // ต้องกำหนด key ใช้ค่าตามนี้ได้เลย
        onDismissed: (_) => Navigator.of(context).pop(), // ปัดลงเพื่อปิด             
        child: Scaffold(
          appBar: AppBar( 
              leading: IconButton(
                onPressed: (){
                  Navigator.of(context).pop();
                }, 
                icon: FaIcon(FontAwesomeIcons.times, color: Colors.black,), 
              ),
              elevation: 0.0,
              actions: <Widget>[ // 
                IconButton(
                  onPressed: () async {
                    FocusScope.of(context).unfocus(); // ยกเลิดโฟกัส ให้แป้นพิมพ์ซ่อนไป
                    await _saveFile(file, _textData.text); // เขียนข้อมูลที่กรอกใหม่ลงไฟล์
                    // จำลองแสดงแจ้งเมื่อบันทึกสำเร็จ
                    ScaffoldMessenger.of(context)
                        .showSnackBar(SnackBar(content: Text('Save data successful')));
                  }, // สร้างโฟลเดอร์ใหม่
                  icon: FaIcon(FontAwesomeIcons.solidSave, color: Colors.black,), 
                ),
              ],
          ),
          body: FutureBuilder<String?>(
            future: _readFile(file), // ข้อมูล future 
            builder: (context, snapshot) { // สร้าง widget เมื่อได้ค่า snapshot ข้อมูลสุดท้าย
              if (snapshot.hasData) { // ถ้าได้ค่าข้อมูลสุดท้าย
                return Form( // ใช้งานฟอร์ม
                  key: _formKey, // กำหนด key
                  child: Container(
                          child: SingleChildScrollView(
                            child: Padding(
                              padding: const EdgeInsets.all(0.0),
                              child: Column(
                                  mainAxisAlignment: MainAxisAlignment.start,
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: <Widget>[
                                    TextFormField(
                                      controller: _textData, // ใช้ข้อความจาก controller
                                      maxLines: 50,
                                      keyboardType: TextInputType.multiline,
                                      decoration: InputDecoration(
                                        border: InputBorder.none,
                                        hintText: "Enter a message",
                                        fillColor: Colors.grey[30],
                                        filled: true,
                                      ),
                                    ),
                                  ],
                              ),
                            ),
                          )
                      ),
                );
              } else if (snapshot.hasError) { // ถ้ามี error
                return Text('${snapshot.error}');
              }
              // ค่าเริ่มต้น, แสดงตัว Loading.
              return const Center(child: CircularProgressIndicator());
            },              
          ),
        ),
      );
    },
  );
}
 
    จากโค้ดหน้า TextEditor ข้างต้นที่เราสร้าง จะมีส่วนทำงานหลัก 3 จุดคือ การดึงข้อมูลเดิมมาแสดงใน
TextFormField ด้วยคำสั่ง _readFile(file)  ส่วนที่สอง การกำหนด controller ให้กับ TextFormField เพื่อ
เชื่อมข้อมูลกับ TextFormField เข้าด้วยกัน และสุดท้ายส่วนของการบันทึกข้อมูล เมื่อกดที่ปุ่มบันทึก
 
IconButton(
  onPressed: () async {
    FocusScope.of(context).unfocus(); // ยกเลิดโฟกัส ให้แป้นพิมพ์ซ่อนไป
    await _saveFile(file, _textData.text); // เขียนข้อมูลที่กรอกใหม่ลงไฟล์
    // จำลองแสดงแจ้งเมื่อบันทึกสำเร็จ
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text('Save data successful')));
  }, // สร้างโฟลเดอร์ใหม่
  icon: FaIcon(FontAwesomeIcons.solidSave, color: Colors.black,), 
),
 
    เมื่อเรามีการเพิ่ม ลบ หรือแก้ไขข้อมุลใน TextFormField เรียบร้อยแล้ว เราต้องการบันทึกเมื่อกดที่ปุ่มบันทึก
ก็จะทำงานตามคำสั่งด้านบน โดยในส่วนของการบันทึกข้อมูล ก็จะใช้งานฟังก์ชั่น _saveFile() ที่ส่งค่าชื่อไฟล์
ที่บันทึก กับข้อมูลใน TextFormField ที่จะบันทึกไปทำงาน
    เมื่อเราได้ฟังก์ชั่น _viewFile() ที่รับค่า context และ  FileSystemEntity ที่คืนค่าเป็นหน้า app มาแล้ว
เราก็เพิ่มการเรียกใช้งาน สำหรับเปิดหน้านี้ ดังนี้
 
Navigator.of(context).push(_viewFile(context, _folders![index]!));  
 
    จะเห็นปกติคำสั่ง push() เราจะใช้กับ class Route หน้าต่างๆ ที่เรามักจะสร้างเป็นไฟล์ใหม่ แต่ในที่นี้เราใช้
เป็นฟังก์ชั่นแทน โดยส่งค่า context และ _folders![index]! ไปตามค่า parameter ที่กำหนด
 
    ต่อไปมาดูส่วนของสองฟังก์ชั่นสุดท้าย คำสั่งสำหรับอ่านข้อมูลจากไฟล์
 
  // อ่านข้อมูลจากไฟล์
  Future<String>? _readFile(file) async {
    var _text = '';
    final _file = File(file.path);
    Stream<String> lines = _file.openRead()
      .transform(utf8.decoder)       // Decode bytes to UTF-8.
      .transform(LineSplitter());    // Convert stream to individual lines.    
    try {
      await for (var line in lines) {
        _text += '${line}\n';
      }
      print('File is now closed.');
    } catch (e) {
      print(e);
    }   
    setState(() {
      _textData.value = TextEditingValue(text: _text);
    });    
    return _text;
  }
 
    ในที่นี้เราเลือกอ่านข้อมูลจากไฟล์ในรูปแบบของ stream ข้อมูลจะมาในรูปแบบ List<int>
หรือ อาเรย์ของกลุ่มตัวเลข interger หรือฐาน 10 หรือเข้าใจในรูปแบบว่าข้อมูลในระดับ bytes
ดังนั้นเมื่อได้ข้อมูลมาแล้วก็จำเป็นจะต้องแปลงเป็นข้อความหรือตัวอักษรที่ถูกต้องการนำมาแสดง
การใช้งานในรูปแบบ stream จะมีประโยชน์กรณีใช้งานกับไฟล์ที่ขนาดใหญ่ได้อย่างมีประสิทธิภาพ
    เมื่อได้ข้อมูล stream ในตัวแปร lines ที่ข้อมูลแยกมาแต่ละบรรทัดแล้ว เราก็นำมาวนลูปรับค่าแต่ละ
บรรทัดไว้ในตัวแปร _text ก่อนนำไปใช้งาน ในตัวอย่างก่อน return ค่ากลับมา เราก็นำค่าที่ได้ไปกำหนด
ให้กับตัวแปร controller เพื่อใช้งาน
    กรณีไม่ต้องการใช่งานแบบ stream ก็สามารถกำหนดเป็นดังนี้
 
  // อ่านข้อมูลจากไฟล์
  Future<String>? _readFile(file) async {
    var _text = '';
    final _file = File(file.path);
    try {
      _text = await _file.readAsString();
      print('File is now closed.');
    } catch (e) {
      print(e);
    }   
    setState(() {
      _textData.value = TextEditingValue(text: _text);
    });    
    return _text;
  }
 
    คำสั่งนี้หลักๆ ก็คืออ่านข้อมูลจากไฟล์ แล้วนำไปแสดงหรือแก้ไข    
 
    ต่อไปเป็นส่วนของคำสั่ง การบันทึกข้อมูลลงไฟล์
 
  // บันทึกข้อมูลข้อความลงไฟล์
  Future<File?> _saveFile(file, str) async {
    File? _file = File(file.path);    
    try{
     // แบบ ใช้ stream
      var sink = _file.openWrite();
      sink.write(str);
      sink.close();    
      // แบบ ไม่ใช้ stream
    //  await _file.writeAsString(str);
    }catch(e){
      print(e);
    }
    return _file;
  }
 
    รูปแบบการทำงานก็น่าจะพอดูไม่ยาก ส่งไฟล์ กับข้อมูลที่จะเขียนมาทำการเขียนลงไปในไฟล์ ในตัวอย่าง
ใช้แบบ stream สามารถเลือกใช้งานแบบไม่ใช้ stream ได้ ตามรูปแบบที่ปิดคอมเมนท์ไว้
 

    ไฟล์ home.dart

 
import 'dart:io';
import 'dart:convert';
import 'dart:async';

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; // เก็บ โฟลเดอร์ที่กำลังใช้งาน

  // ตัวแปรเก็บ index รายการที่เลือก
  List<int> _selectedItems = [];

  // สร้างฟอร์ม key หรือ id ของฟอร์มสำหรับอ้างอิง
  final _formKey = GlobalKey<FormState>();    
  // กำหนดตัวแปรรับค่า
  final _textData = TextEditingController();      

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

  @override
  void dispose() {
    _textData.dispose(); 
    super.dispose();
  }

  // เปิดโฟลเดอร์ และแสดงรายการในโฟลเดอร์
  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);
    }
    _selectedItems.clear(); // ล้างค่าการเลือกทั้งหมด
  }

  // คำสังลบไฟล์
  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 _deleteAll() async {
    bool _confirm; // สร้างตัวแปรรับค่า ยืนยันการลบ
    _confirm = await showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text("Confirm"),
          content: const Text("Are you sure you wish to delete selected 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"),
            ),
          ],
        );
      },
    );
    if(_confirm){ // ถ้ายืนยันการลบ เป็น true
      try{
        // วนลูป index แล้วอ้างอึงข้อมูลไฟล์ จากนั้นใช้คำสั่ง delete() แบบรองรับการลบข้อมูลด้านในถ้ามี
        // ในกรณีเป็นโฟลเดอร์
        _selectedItems.forEach((index) async {
          await _folders![index]!.delete(recursive: true);
        });
      }catch(e){
        print(e);
      }
      // โหลดข้อมูลใหม่อีกครั้ง    
      setState(() {
        print("wow");
        _setPath(_currentFolder!);
      });   
    } 
  }

  // จำลองสร้างไฟล์ใหม่
  void _newFile() async {
    String filename = "${DateTime.now().millisecondsSinceEpoch}.txt";
    String newFile = "${_currentFolder!.path}/${filename}";
    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);
      }
    }
    // โหลดข้อมูลใหม่อีกครั้ง
    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);
      }
    }
    // โหลดข้อมูลใหม่อีกครั้ง    
    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), 
                ),
                if(_selectedItems.isNotEmpty)
                IconButton(
                    onPressed: _deleteAll, 
                    icon: FaIcon(FontAwesomeIcons.trashAlt), 
                  ),
              ],
          ),
          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(
                                  direction: DismissDirection.horizontal,
                                  key: UniqueKey(),
                                  // dismissThresholds: const { DismissDirection.endToStart:1.0, DismissDirection.startToEnd:1.0},
                                  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(
                                    selected: _selectedItems.contains(index) ? true : false,
                                    leading: isFolder
                                    ? FaIcon(FontAwesomeIcons.solidFolder)
                                    : FaIcon(FontAwesomeIcons.file),
                                    trailing: Visibility(
                                      visible: _selectedItems.contains(index) ? true : false,
                                      child: FaIcon(FontAwesomeIcons.checkCircle),
                                    ),
                                    title: Text('${fileName}'),
                                    onLongPress: (){
                                      if(! _selectedItems.contains(index)){
                                        setState(() {
                                          _selectedItems.add(index);
                                        });
                                      }                                      
                                    },
                                    onTap: (isFolder==true)
                                      ? (){ // กรณีเป้นโฟลเดอร์
                                        if(_selectedItems.contains(index)){
                                          setState(() {
                                            _selectedItems.removeWhere((val) => val == index);
                                          });
                                        }else{
                                          if(_selectedItems.isNotEmpty){
                                            setState(() {
                                              _selectedItems.add(index);
                                            });                                            
                                          }else{
                                            _setPath(_folders![index]!); // ถ้ากด ให้ทำคำสั่งเปิดโฟลเดอร์
                                          }  
                                        }    
                                      }
                                      : (){
                                        if(_selectedItems.contains(index)){
                                          setState(() {
                                            _selectedItems.removeWhere((val) => val == index);
                                          });
                                        }else{
                                          if(_selectedItems.isNotEmpty){
                                            setState(() {
                                              _selectedItems.add(index);
                                            });    
                                          }else{
                                            Navigator.of(context).push(_viewFile(context, _folders![index]!));  
                                          }
                                        }
                                      }, // กรณีเป็นไฟล์ 
                                  )
                                );
                              }else{
                                return Container();
                              }
                            },
                            separatorBuilder: (BuildContext context, int index) => const Divider(height: 1,),                    
                          )
                        : const Center(child: Text('No items')), // กรณีไม่มีรายการ
                      ),
                  ],
          ),
          drawer: SideMenu(),
      );
  }

  // อ่านข้อมูลจากไฟล์
  Future<String>? _readFile(file) async {
    var _text = '';
    final _file = File(file.path);
    // แบบ ใช้ stream
     Stream<String> lines = _file.openRead()
      .transform(utf8.decoder)       // Decode bytes to UTF-8.
      .transform(LineSplitter());     // Convert stream to individual lines.    
    try {
      // แบบ ใช้ stream      
      await for (var line in lines) {
        _text += '${line}\n';
      }
      // แบบ ไม่ใช้ stream
     // _text = await _file.readAsString();
      print('File is now closed.');
    } catch (e) {
      print(e);
    }   
    setState(() {
      _textData.value = TextEditingValue(text: _text);
    });    
    return _text;
  }

  // บันทึกข้อมูลข้อความลงไฟล์
  Future<File?> _saveFile(file, str) async {
    File? _file = File(file.path);    
    try{
     // แบบ ใช้ stream
      var sink = _file.openWrite();
      sink.write(str);
      sink.close();    
      // แบบ ไม่ใช้ stream
    //  await _file.writeAsString(str);
    }catch(e){
      print(e);
    }
    return _file;
  }

  // สร้างฟังก์ชั่น ที่คืนค่าเป็น route ของ object ฟังก์ชั่นนี้ มี context และ product เป็น parameter
  Route<Object?> _viewFile(BuildContext context, FileSystemEntity file) { 
    return DialogRoute<void>(
      context: context,
      builder: (context) {
        return Dismissible( // คืนค่าเป็น dismissible widget
          direction: DismissDirection.vertical, // เมื่อปัดลงในแนวตั้ง
          key: const Key('key'), // ต้องกำหนด key ใช้ค่าตามนี้ได้เลย
          onDismissed: (_) => Navigator.of(context).pop(), // ปัดลงเพื่อปิด             
          child: Scaffold(
            appBar: AppBar( 
                leading: IconButton(
                  onPressed: (){
                    Navigator.of(context).pop();
                  }, 
                  icon: FaIcon(FontAwesomeIcons.times, color: Colors.black,), 
                ),
                elevation: 0.0,
                actions: <Widget>[ // 
                  IconButton(
                    onPressed: () async {
                      FocusScope.of(context).unfocus(); // ยกเลิดโฟกัส ให้แป้นพิมพ์ซ่อนไป
                      await _saveFile(file, _textData.text); // เขียนข้อมูลที่กรอกใหม่ลงไฟล์
                      // จำลองแสดงแจ้งเมื่อบันทึกสำเร็จ
                      ScaffoldMessenger.of(context)
                          .showSnackBar(SnackBar(content: Text('Save data successful')));
                    }, // สร้างโฟลเดอร์ใหม่
                    icon: FaIcon(FontAwesomeIcons.solidSave, color: Colors.black,), 
                  ),
                ],
            ),
            body: FutureBuilder<String?>(
              future: _readFile(file), // ข้อมูล future 
              builder: (context, snapshot) { // สร้าง widget เมื่อได้ค่า snapshot ข้อมูลสุดท้าย
                if (snapshot.hasData) { // ถ้าได้ค่าข้อมูลสุดท้าย
                  return Form( // ใช้งานฟอร์ม
                    key: _formKey, // กำหนด key
                    child: Container(
                            child: SingleChildScrollView(
                              child: Padding(
                                padding: const EdgeInsets.all(0.0),
                                child: Column(
                                    mainAxisAlignment: MainAxisAlignment.start,
                                    crossAxisAlignment: CrossAxisAlignment.start,
                                    children: <Widget>[
                                      TextFormField(
                                        controller: _textData, // ใช้ข้อความจาก controller
                                        maxLines: 50,
                                        keyboardType: TextInputType.multiline,
                                        decoration: InputDecoration(
                                          border: InputBorder.none,
                                          hintText: "Enter a message",
                                          fillColor: Colors.grey[30],
                                          filled: true,
                                        ),
                                      ),
                                    ],
                                ),
                              ),
                            )
                        ),
                  );
                } else if (snapshot.hasError) { // ถ้ามี error
                  return Text('${snapshot.error}');
                }
                // ค่าเริ่มต้น, แสดงตัว Loading.
                return const Center(child: CircularProgressIndicator());
              },              
            ),
          ),
        );
      },
    );
  }

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


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



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









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









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





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







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