เนื้อหาตอนต่อไปนี้ เราจะต่อยอดจากบทความที่ผ่านมา
เกี่ยวกับการจัดการข้อมูล future ซึ่งมีลำดับขั้นตอนเบื้องต้น
ตามที่ได้อธิบายไปแล้ว อยากไรก็ตาม การนำไปใช้งานจริงๆ เราจำเป็น
ต้องจัดการข้อมูลมากยิ่งขึ้น และอาจจะเจอปัญหาเกี่ยวกับโครงสร้างของ
ui ในขั้นตอนการจัดวางรูปแบบการแสดงผล เหล่านี้เราต้องฝึกบ่อยๆ
จึงจะสามารถรวบรวมแนวทางและหาวิธีแก้ป้ญหาต่างๆ ได้
ทบทวนกระบวนการโหลดข้อมูล Future และการใช้งาน ValueNotifier
สิ่งที่เราจะทำและเรียนรู้ในบทความนี้
- เราจะทำการดึงข้อมูลจริงๆ จาก server แต่ใช้เป็นข้อมูลจำลอง
- ทำการ cache ข้อมูลเป็นไฟล์ให้สามารถใช้งานข้อมูลได้เร็วขึ้น
- เข้าใจภาพรวมการจัดหารหน้าแสดงรายการมากขึ้น
ประยุกต์การแสดงข้อมูลจาก API ของ Server
จริงๆแล้ว เนื้อหาเหล่านี้เรามีข้อมูลและตัวอย่างการแนะนำทั้งหมดในบทความที่ผ่านๆ มาแล้ว
อย่างในกรณีที่เราจะประยุกต์นี้ ก็คล้ายกับบทความ
จัดการข้อมูล Model และแนวทางการนำมาใช้งาน ใน Flutter http://niik.in/1041
การใช้งาน Path Provider และการเชียนอ่าน File ใน Flutter http://niik.in/1066
การใช้งาน Http ดึงข้อมูลจาก Server มาแสดงใน Flutter http://niik.in/1038
อย่างไรก็ตาม ก่อนอื่นเลย ให้เราเตรียมส่วนของ package ต่างๆ ดังนี้ไว้ในโปรเจ็ค
http: ^1.2.2 path_provider: ^2.1.4 cached_network_image: ^3.4.1 intl: ^0.19.0
http สำหรับดึงข้อมูลจาก server
path_provider สำหรับจัดการเกี่ยวกับไฟล์ ที่เราจะสร้างเป็นไฟล์ cache
cached_network_image สำหรับ cache รูปที่เราแสดง
intl จัดการเกี่ยวกับวันที่และเวลา (อาจจะยังไมได้ใช้ในบทความนี้ แต่แอปส่วนใหญ่จะใช้งาน)
โค้ดตัวอย่างต่อไปนี้ เราพิมพ์เติมจากบทความตอนที่ผ่านมา คือเพิ่มส่วนของ Data Model ของ
ข้อมูลสินค้า ที่เราจะไปดึงจาก api มาใช้งาน ในโฟลเดอร์ lib > models > product_model.dart
ในท้ายบทความมีไฟล์ทั้งหมดให้ดาวน์โหลด
จากนั้นเรากำหนดในไฟล์ home.dart เป็นดังนี้
ไฟล์ home.dart
import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../models/product_model.dart'; class Home extends StatefulWidget { static const routeName = '/home'; const Home({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _HomeState(); } } class _HomeState extends State<Home> { // สร้างตัวแปรที่สามารถแจ้งเตือนการเปลี่ยนแปลงค่า final ValueNotifier<bool> _visible = ValueNotifier<bool>(false); // กำนหดตัวแปรข้อมูล products Future<List<Product>> _products = Future.value([]); // ตัว ScrollController สำหรับจัดการการ scroll ใน ListView final ScrollController _scrollController = ScrollController(); // จำลองใช้เป็นแบบฟังก์ชั่น ให้เสมือนดึงข้อมูลจาก server Future<String> fetchData() async { print("debug: do function"); final response = await Future<String>.delayed( const Duration(seconds: 2), () { return 'Data Loaded \n${DateTime.now()}'; }, ); return response; } Future<void> _refresh() async { _visible.value = true; setState(() { _products = fetchProduct(); }); } @override void initState() { print("debug: Init"); super.initState(); _products = fetchProduct(); } @override void dispose() { _scrollController.dispose(); _visible.dispose(); // Dispose the ValueNotifier super.dispose(); } @override Widget build(BuildContext context) { print("debug: build"); return Scaffold( appBar: AppBar( title: Text('Home'), actions: [ IconButton( onPressed: () { _refresh(); }, icon: const Icon(Icons.refresh_outlined), ) ], ), body: ListView( padding: const EdgeInsets.all(8.0), children: [ ValueListenableBuilder<bool>( valueListenable: _visible, builder: (context, visible, child) { return Visibility( visible: visible, child: const LinearProgressIndicator( backgroundColor: Colors.white60, ), ); }, ), FutureBuilder<List<Product>>( // ชนิดของข้อมูล future: _products, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) {} if (snapshot.connectionState == ConnectionState.done) { WidgetsBinding.instance.addPostFrameCallback((_) { // Change state after the build is complete _visible.value = false; if (_scrollController.hasClients) { //เช็คว่ามีตัว widget ที่ scroll ได้หรือไม่ ถ้ามี // เลื่อน scroll มาด้านบนสุด _scrollController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); } }); } if (snapshot.hasData) { // แสดงทั้งหมด final items = snapshot.data!.toList(); // แสดงแค่ 10 รายการ // final items = snapshot.data!.take(10).toList(); return Column( children: [ Container( // สร้างส่วน header ของลิสรายการ padding: const EdgeInsets.all(5.0), decoration: BoxDecoration( color: Colors.orange.withAlpha(100), ), child: Row( children: [ Text( 'Total ${items.length} items'), // แสดงจำนวนรายการ ], ), ), SizedBox( // ปรับความสูงขางรายการทั้งหมด การ ลบค่า เพื่อให้ข้อมูลแสดงเต็มพื้นที่ // หากมี appbar ควรลบ 100 ถ้าไม่มีควรลบ 200 หรือค่าอื่นๆ ตามเหมาะสม height: MediaQuery.of(context).size.height - 200, child: snapshot.data!.isNotEmpty // กำหนดเงื่อนไขตรงนี้ ? RefreshIndicator( onRefresh: () async { _refresh(); }, // Function to call when the user pulls to refresh child: ListView.separated( // กรณีมีรายการ แสดงปกติ controller: _scrollController, // กำนหนด controller ที่จะใช้งานร่วม itemCount: items.length, itemBuilder: (context, index) { Product product = items[index]; Widget card; // สร้างเป็นตัวแปร card = Card( margin: const EdgeInsets.all( 5.0), // การเยื้องขอบ child: Column( children: [ ListTile( leading: Image.network( product.image, width: 100.0, ), title: Text(product.title), subtitle: Text( 'Price: \$ ${product.price}'), trailing: Icon(Icons.more_vert), onTap: () {}, ), ], )); return card; }, separatorBuilder: (BuildContext context, int index) => const SizedBox(), ), ) : const Center( child: Text('No items')), // กรณีไม่มีรายการ ), ], ); } else if (snapshot.hasError) { return Center(child: Text('${snapshot.error}')); } return const Center(child: CircularProgressIndicator()); }, ), ], ), floatingActionButton: ValueListenableBuilder<bool>( valueListenable: _visible, builder: (context, visible, child) { return (visible == false) ? FloatingActionButton( onPressed: () { _refresh(); }, shape: const CircleBorder(), child: const Icon(Icons.refresh), ) : SizedBox.shrink(); }, ), ); } } // สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Product Future<List<Product>> fetchProduct() async { // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด String url = 'https://fakestoreapi.com/products'; final response = await http.get(Uri.parse(url)); // เมื่อมีข้อมูลกลับมา if (response.statusCode == 200) { // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Product // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseProducts // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body return compute(parseProducts, response.body); } else { // กรณี error throw Exception('Failed to load product'); } } // ฟังก์ชั่นแปลงข้อมูล JSON String data เป็น เป็นข้อมูล List<Product> List<Product> parseProducts(String responseBody) { final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>(); return parsed.map<Product>((json) => Product.fromJson(json)).toList(); }
ไฟล์เริ่มต้น ผลลัพธ์ที่ได้
กระบวนการทำงานจะคล้ายๆ กับบทความที่แล้ว คือ เปิดเข้ามา ทำการโหลดข้อมูลจาก server จากนั้น
นำมาแสดงลิสรายการสินค้า สามารถ refresh ได้หลากหลาย ทั้งกดที่ปุ่มที่กำหนด หรือ ปัดลงเพื่อ
refresh มีการกำหนด ให้เลื่อนรายการกลับมาด้านบน หากโหลดข้อมูลใหม่เรียบร้อยแล้ว เช่น สมมติ
ว่าเราเลื่อนไปอยู่รายการท้ายๆ แล้วทำการ refresh ข้อมูล เมื่อข้อมูลโหลดเสร็จ ก็จะทำการเลื่อน
หน้าจอมาด้านบน เราได้เพิ่มโค้ดของการกำหนดขนาดความสูงของ SizedBox widget ให้สัมพันธ์
กับขนาดความสูงของลิสรายการข้อมูลด้วย MediaQuery.of(context).size.height และเราได้
ทำการจับในส่วนของฟังก์ชั่นที่ดึงข้อมูลจาก server ออกมาข้างนอก State class เพื่อไม่ให้ส่วน
ของ state ดูซับซ้อนจัดการยากเกินไป อย่างไรก็ดี การแยกส่วนคำสั่งต่างๆ ก็จะทั้งข้อดีข้อเสียแตกต่าง
กันไป ขึ้นอยู่กับการประยุกต์ใช้งาน เช่น ถ้าฟังก์ชั่นดึงข้อมูลไม่ได้ซับซ้อนมาก เราก็สามารถเขียนไว้
ใน class state ได้เลยไม่ต้องแยกออกมาก็ได้ แต่ถ้าว่า คำสั่งการจัดการมีความซับซ้อนขึ้น การแยก
ออกมาอีกไฟล์หรือออกมาข้างนอกก็จะทำให้การจัดการทำได้ง่ายขึ้น
พยายามศึกษาโค้ดข้างต้นแต่ละบรรทัด ให้เข้าใจ เราจะได้สามารถนำไปประยุกต์ เมื่อใช้งานจริงๆ
โค้ดตัวอย่างข้างต้น ยังไม่มีการสร้างไฟล์ cache แต่อย่างไร เป็นการดึงข้อมุลปกติทั่วไป นั่นคือเมื่อเข้า
มาหน้าแอปหน้านี้ก็จะทำการไปดึงข้อมูลจาก server ทุกๆ ครั้ง *ดาวน์โหลดไฟล์ตัวอย่างได้ที่ท้าย
บทความ
การ Cache ข้อมูลจาก Server มาเก็บไว้ในไฟล์เพื่อเรียกใช้งาน
ต่อไปเรามาสู่ขั้นตอนการจัดการเกี่ยวกับการ cache ข้อมูล โดยจะมีเงื่อนไขการทำงานดึงนี้คือ
ทุกครั้งที่ผู้ใช้ทำการ refresh ข้อมูล เราจะทำการตรวจสอบไฟล์ cache ที่เครืองการว่ามือหรือไม่
ถ้ามีเราจะใช้ไฟล์ cache ที่เครื่องมาแสดงแทนการไปโหลดที่ฝั่ง server ขั้นต้นเป็นแบบนี้ก่อน ดังนั้น
ส่วนที่เราจะดูก็คือฟังก์ชั่น การโหลดข้อมูล ในที่นี้คือ
// สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Product Future<List<Product>> fetchProduct() async { // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด String url = 'https://fakestoreapi.com/products'; final response = await http.get(Uri.parse(url)); // เมื่อมีข้อมูลกลับมา if (response.statusCode == 200) { // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Product // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseProducts // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body return compute(parseProducts, response.body); } else { // กรณี error throw Exception('Failed to load product'); } }
เนื้อหาเกี่ยวกับการจัดการไฟล์ เขียนไฟล์ อ่านไฟล์มีรายละเอียดแล้วที่บทความ http://niik.in/1066
เราจะปรับเป็นดังนี้
// สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Product Future<List<Product>> fetchProduct() async { String _currentPath = ''; // เก็บ path ปัจจุบัน final appDocumentsDirectory = await getApplicationDocumentsDirectory(); _currentPath = appDocumentsDirectory.path; String filename = "product_cache.json"; String readFile = "$_currentPath/$filename"; String _jsonData = ''; final _file = File(readFile); final isExits = await _file.exists(); try { if (isExits) { print("debug: read from file"); _jsonData = await _file.readAsString(); return compute(parseProducts, _jsonData); } else { // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด String url = 'https://fakestoreapi.com/products'; final response = await http.get(Uri.parse(url)); // เมื่อมีข้อมูลกลับมา if (response.statusCode == 200) { print("debug: load form server"); final myfile = _file; final isExits = await myfile.exists(); // เช็คว่ามีไฟล์หรือไม่ if (!isExits) { // ถ้ายังไม่มีไฟล์ try { await myfile.writeAsString(response.body); } catch (e) { throw Exception('error: ${e}'); } } else { try { await myfile.writeAsString(response.body); } catch (e) { throw Exception('error: ${e}'); } } // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Product // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseProducts // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body return compute(parseProducts, response.body); } else { // กรณี error throw Exception('Failed to load product'); } } } catch (e) { throw Exception('error: ${e}'); } }
เมื่อมีการใช้งานในลักษณะข้างต้น เราจะต้อง import package ต่างๆ ที่จำเป็น เช่น ในตัวอย่างนี้
import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; // import 'package:intl/intl.dart'; // จัดรูปแบบวันทีและเวลา http://niik.in/1047 import 'package:cached_network_image/cached_network_image.dart'; import 'package:path_provider/path_provider.dart'; import '../models/product_model.dart';
กลับมาที่ส่วนของโค้ดกันต่อ จริงๆ ก็ไม่ซับซ้อนอะไร เริ่มต้นเรากำหนดชื่อไฟล์สำหรับเป็นไฟล์ cache
ชื่อว่า product_cache.json ซึ่งในการใช้งานครั้งแรก ไฟล์นี้จะยังไม่ถูกสร้าง ดังนั้นจึงเป็นการไป
โหลดข้อมูลจาก server จากนั้น ทำการสร้างไฟล์ แล้วนำข้อมูล ที่โหลดจาก server มาบันทึก
ลงในไฟล์ cache ดังกล่าว เมื่อกลับมาครั้งที่สอง ก็จะเป็นการไปโหลดข้อมุลจากไฟล์แทนการไปโหลด
ที่ server โดยตรง ทำให้ในการโหลดข้อมูล ก็จะทำงานเร็วขึ้น
แต่สิ่งต่อมาที่เราต้องพิจารณาต่อก็คือ แล้วการโหลดข้อมูลครั้งที่สองเป็นต้นไป เป็นการไปอ่านจากไฟล์
ตลอดแบบนี้ข้อมูลจะอัพเดทหรือไม่ เพราะข้อมูลอาจจะเป็นข้อมูลเก่า เราจะแก้ไขยังไง วิธีต่อไปก็คือ เรา
จะกำหนดให้ หากเป็นการโหลดข้อมูลปกติ จะใช้การเรียกจากไฟล์ แต่ถ้าทำการเรียกใช้ฟังก์ชั่น refresh
จะให้ทำการไปโหลดที่ฝั่ง server แล้วทำการเขียนข้อมูลใหม่ลงไฟล์ เพื่อให้เป็นข้อมูลอัพเดท ดังนั้นใน
ขั้นตอนการ refresh เราจะต้องมีการกำหนด parameter สำหรับให้ไปโหลดจาก server เป็นดังนี้
ในฟังก์ชั่น fetchProduct จะแก้ไข 2 จุดคือ
// กำหนด parameter สำหรับการโหลดค่าใหม่เข้าไป ชื่อว่า reload Future<List<Product>> fetchProduct({reload}) async { // และส่วนของการอ่านจากไฟล์ จะอ่านจากไฟล์กรณีไม่ได้กำหนด reload เข้ามา if(isExits && reload==null){
ต่อไปในส่วนของฟังก์ชั่น refresh
// เรากำหนด argument เพิ่ม reload มีค่าเท่ากับ true เข้าไป // นั่นคือ ทุกครั้งที่กด refresh ก็จะทำการไปโหลดข้อมูลใหม่มาแสดง และบันทึก // cache ข้อมูลล่าสุดไว้ Future<void> _refresh() async { _visible.value = true; setState(() { _products = fetchProduct(reload: true); }); }
ต่อไป เราจัดการส่วนของการแสดงรูปภาพต่อ เดิมจะเป็น
ListTile( leading: Image.network( product.image, width: 100.0, ), title: Text(product.title), subtitle: Text( 'Price: \$ ${product.price}'), trailing: Icon(Icons.more_vert), onTap: () {}, ),
เราใช้งาน cached_network_image package จัดการเป็นดังนี้
ListTile( leading: CachedNetworkImage( imageUrl: product.image, width: 100.0, placeholder: (context, url) => Center( child: SizedBox( // Adjust the size as needed width: 40.0, height: 40.0, child: CircularProgressIndicator(), // Show loading indicator ), ), errorWidget: (context, url, error) => Icon(Icons .error), // Show error icon if loading fails ), title: Text(product.title), subtitle: Text( 'Price: \$ ${product.price}'), trailing: Icon(Icons.more_vert), onTap: () {}, )
ไฟล์ home.dart รองรับการ cache
import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; // import 'package:intl/intl.dart'; // จัดรูปแบบวันทีและเวลา http://niik.in/1047 import 'package:cached_network_image/cached_network_image.dart'; import 'package:path_provider/path_provider.dart'; import '../models/product_model.dart'; class Home extends StatefulWidget { static const routeName = '/home'; const Home({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _HomeState(); } } class _HomeState extends State<Home> { // สร้างตัวแปรที่สามารถแจ้งเตือนการเปลี่ยนแปลงค่า final ValueNotifier<bool> _visible = ValueNotifier<bool>(false); // กำนหดตัวแปรข้อมูล products Future<List<Product>> _products = Future.value([]); // ตัว ScrollController สำหรับจัดการการ scroll ใน ListView final ScrollController _scrollController = ScrollController(); // สำหรับป้องกันการเรียกโหลดข้อมูลซ้ำในทันที bool _isLoading = false; // จำลองใช้เป็นแบบฟังก์ชั่น ให้เสมือนดึงข้อมูลจาก server Future<String> fetchData() async { print("debug: do function"); final response = await Future<String>.delayed( const Duration(seconds: 2), () { return 'Data Loaded \n${DateTime.now()}'; }, ); return response; } Future<void> _refresh() async { if (_isLoading) return; _visible.value = true; try { setState(() { _isLoading = true; _products = fetchProduct(reload: true); }); } catch (e) { throw Exception('error: ${e}'); } finally { setState(() { _isLoading = false; }); } } @override void initState() { print("debug: Init"); super.initState(); _products = fetchProduct(); } @override void dispose() { _scrollController.dispose(); _visible.dispose(); // Dispose the ValueNotifier super.dispose(); } @override Widget build(BuildContext context) { print("debug: build"); return Scaffold( appBar: AppBar( title: Text('Home'), actions: [ IconButton( onPressed: () async { if (!_isLoading && _visible.value == false) { _refresh(); } }, icon: const Icon(Icons.refresh_outlined), ) ], ), body: ListView( padding: const EdgeInsets.all(8.0), children: [ ValueListenableBuilder<bool>( valueListenable: _visible, builder: (context, visible, child) { return Visibility( visible: visible, child: const LinearProgressIndicator( backgroundColor: Colors.white60, ), ); }, ), FutureBuilder<List<Product>>( // ชนิดของข้อมูล future: _products, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) {} if (snapshot.connectionState == ConnectionState.done) { WidgetsBinding.instance.addPostFrameCallback((_) { // Change state after the build is complete _visible.value = false; if (_scrollController.hasClients) { //เช็คว่ามีตัว widget ที่ scroll ได้หรือไม่ ถ้ามี // เลื่อน scroll มาด้านบนสุด _scrollController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); } }); } if (snapshot.hasData) { // แสดงทั้งหมด final items = snapshot.data!.toList(); // แสดงแค่ 10 รายการ // final items = snapshot.data!.take(10).toList(); double statusBarHeight = MediaQuery.of(context).padding.top; double appBarHeight = kToolbarHeight; // Default height of the AppBar (56.0) double availableHeight = MediaQuery.of(context).size.height - statusBarHeight - appBarHeight - 80; print("debug: ${statusBarHeight+kToolbarHeight+80}"); return Column( children: [ Container( // สร้างส่วน header ของลิสรายการ padding: const EdgeInsets.all(5.0), decoration: BoxDecoration( color: Colors.orange.withAlpha(100), ), child: Row( children: [ Text( 'Total ${items.length} items'), // แสดงจำนวนรายการ ], ), ), SizedBox( // ปรับความสูงขางรายการทั้งหมด การ ลบค่า เพื่อให้ข้อมูลแสดงเต็มพื้นที่ // หากมี appbar ควรลบ 100 ถ้ามีส่วนอื่นเพิ่มให้บวกเพิ่มเข้าไป ตามเหมาะสม // หากไม่มี appbar ควรลบพื้นที่ที่เพิ่มเข้ามาค่าอื่นๆ ตามเหมาะสม height: MediaQuery.of(context).size.height - 136, child: snapshot.data!.isNotEmpty // กำหนดเงื่อนไขตรงนี้ ? RefreshIndicator( onRefresh: () async { if (!_isLoading && _visible.value == false) { _refresh(); } }, child: ListView.separated( // กรณีมีรายการ แสดงปกติ controller: _scrollController, // กำนหนด controller ที่จะใช้งานร่วม itemCount: items.length, itemBuilder: (context, index) { Product product = items[index]; Widget card; // สร้างเป็นตัวแปร card = Card( margin: const EdgeInsets.all( 5.0), // การเยื้องขอบ child: Column( children: [ ListTile( leading: CachedNetworkImage( imageUrl: product.image, width: 100.0, placeholder: (context, url) => Center( child: SizedBox( // Adjust the size as needed width: 40.0, height: 40.0, child: CircularProgressIndicator(), // Show loading indicator ), ), errorWidget: (context, url, error) => Icon(Icons .error), // Show error icon if loading fails ), title: Text(product.title), subtitle: Text( 'Price: \$ ${product.price}'), trailing: Icon(Icons.more_vert), onTap: () {}, ) ], )); return card; }, separatorBuilder: (BuildContext context, int index) => const SizedBox(), ), ) : const Center( child: Text('No items')), // กรณีไม่มีรายการ ), ], ); } else if (snapshot.hasError) { return Center(child: Text('${snapshot.error}')); } return const Center(child: CircularProgressIndicator()); }, ), ], ), floatingActionButton: ValueListenableBuilder<bool>( valueListenable: _visible, builder: (context, visible, child) { return (visible == false) ? FloatingActionButton( onPressed: () async { if (!_isLoading && _visible.value == false) { _refresh(); } }, shape: const CircleBorder(), child: const Icon(Icons.refresh), ) : SizedBox.shrink(); }, ), ); } } // สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Product Future<List<Product>> fetchProduct({reload}) async { String _currentPath = ''; // เก็บ path ปัจจุบัน final appDocumentsDirectory = await getApplicationDocumentsDirectory(); _currentPath = appDocumentsDirectory.path; String filename = "product_cache.json"; String readFile = "$_currentPath/$filename"; String _jsonData = ''; final _file = File(readFile); final isExits = await _file.exists(); try { if (isExits && reload == null) { print("debug: read from file"); _jsonData = await _file.readAsString(); return compute(parseProducts, _jsonData); } else { // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด String url = 'https://fakestoreapi.com/products'; final response = await http.get(Uri.parse(url)); // เมื่อมีข้อมูลกลับมา if (response.statusCode == 200) { print("debug: load form server"); final myfile = _file; final isExits = await myfile.exists(); // เช็คว่ามีไฟล์หรือไม่ if (!isExits) { // ถ้ายังไม่มีไฟล์ try { await myfile.writeAsString(response.body); } catch (e) { throw Exception('error: ${e}'); } } else { try { await myfile.writeAsString(response.body); } catch (e) { throw Exception('error: ${e}'); } } // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Product // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseProducts // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body return compute(parseProducts, response.body); } else { // กรณี error throw Exception('Failed to load product'); } } } catch (e) { throw Exception('error: ${e}'); } } // ฟังก์ชั่นแปลงข้อมูล JSON String data เป็น เป็นข้อมูล List<Product> List<Product> parseProducts(String responseBody) { final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>(); return parsed.map<Product>((json) => Product.fromJson(json)).toList(); }
ตอนนี้ท้้งรูปภาพและข้อมูลก็ได้รับการจัดการเรื่อง cache เรียบร้อยแล้ว การทำงานมีประสิทธิภาพ
มากยิ่งขึ้น เราสามารถปรับปรุงด้านอื่นๆ เพิ่มเติมได้ตามต้องการ เช่น ถ้าทำการ refresh โหลดข้อมูล
จาก server ใหม่ แต่ถ้าไม่สามารถเชื่อมต่อไปยัง server ได้ ก็อาจจะให้เอาข้อมูลเก่ามาแสดง แบบนี้
เป็นต้น
การ cache ข้อมูลในลักษณะของไฟล์ มีข้อดีคือทำงานได้เร็ว และไม่ซับซ้อน อย่างไรก็ดี หากเรามีการ
นำข้อมูลไปใช้งานในแบบที่ซับซ้อน เช่น เป็นข้อมูลที่สามารถค้นหาได้ จัดเรียงข้อมูลได้ หรืออื่นๆ การ
ใช้วิธี cache ในรูปแบบไฟล์อาจจะไม่เหมาะสม กรณีนี้ เราสามารถใช้เป็นรูปแบบบันทึกลง ฐานข้อมูล
Sqlite ได้ อาจจะได้นำเสนอในบทความตอนต่อๆ ไป รอติดตาม
ดังนั้นในการพัฒนาแอป เราอาจจะต้องทดสอบหลายๆ ครั้งปรับปรุงหลายๆ จุดไปเรื่อยๆ เพื่อให้แอปของ
เรามีประสิทธิภาพและทำงานได้ราบรื่นขึ้น