เนื้อหานี้จะมาดูต่อเกี่ยวกับการจัดการไฟล์ โดยเฉพาะ
ไฟล์ text นามสกุลไฟล์ txt เป็นตัวอย่างที่สามารถเอาไป
ปรับใช้งานได้ เช่น ต้องการให้ app มีส่วนของการจดบันทึก
ข้อมูลคล้ายๆ กับสมุดจดบันทึกที่เก็บข้อมูลไว้ในไฟล์ และ
สามารถเปิดขึ้นมาอ่านหรือแสดงได้ เนื้อหานี้ต่อเนื่องจากตอนที่
แล้ว ถึงจะไม่ได้สัมพันธ์กับเนื้อหาที่ผ่านมาแต่ก็ใช้โค้ดต่อเนื่อง
ทบทวนตอนที่แล้วได้ที่บทความ
การทำ Selected Item ใน ListView เพื่อจัดการ ใน Flutter http://niik.in/1068
https://www.ninenik.com/content.php?arti_id=1068 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/1068
ลำดับสิ่งที่เราจะทำ
ในตัวอย่างที่ผ่านๆมา เรามีปุ่มสำหรับสร้างไฟล์และโฟลเดอร์ตรงมุมบนขวา ซึ่งกำหนดค่าแบบ
ตายตัวเป็นชื่อ 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 ได้ ตามรูปแบบที่ปิดคอมเมนท์ไว้
ไฟล์ explorer.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'; 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; // เก็บ โฟลเดอร์ที่กำลังใช้งาน // ตัวแปรเก็บ index รายการที่เลือก List<int> _selectedItems = []; // สร้างฟอร์ม key หรือ id ของฟอร์มสำหรับอ้างอิง final _formKey = GlobalKey<FormState>(); // กำหนดตัวแปรรับค่า final _textData = TextEditingController(); @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); }); } @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!); }); } @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), ), 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.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( 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')), // กรณีไม่มีรายการ ), ], ), ); } // อ่านข้อมูลจากไฟล์ 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 ข้อมูล
ก็ได้ เนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม