เนื้อหาในตอนต่อไปนี้ เราจะมีดูเกี่ยวกับการใช้งาน Autocomplete
ซึ่งเป็น widget หนึ่งของ flutter เราจะต่อยอดจากตอนที่แล้ว เกี่ยวกับ
เนื้อหาของรายการสินค้า เราจะใช้งาน autocomplete มาช่วยในการค้น
หารายการสินค้า เป็นแนวทางในการนำไปประยุกต์ใช้เพิ่มเติมต่อไป
*ท้ายบทความมีโค้ดตัวอย่างให้โหลด
เนื้อหาตอนที่แล้วดูได้ที่
การใช้งาน Shimmer effect แสดงการรอโหลดข้อมูลใน Flutter http://niik.in/1111
Autocomplete ใน flutter คืออะไร ใช้ทำอะไร
ใน Flutter นั้น Autocomplete เป็น widget ที่ใช้สร้าง autocomplete input field
ซึ่งช่วยให้ผู้ใช้สามารถกรอกข้อมูลลงในฟิลด์แล้วแสดงรายการคำแนะนำที่ตรงกับสิ่งที่พิมพ์ได้ทันที
โดยแสดงรายการที่ตรงกับข้อความที่ผู้ใช้พิมพ์ในแบบ dropdown list และให้ผู้ใช้เลือกคำแนะนำได้
การใช้งานหลักของ Autocomplete
- ใช้เมื่อต้องการให้ผู้ใช้กรอกข้อมูลและแสดงตัวเลือกที่เป็นไปได้แบบอัตโนมัติเพื่อลดความยุ่งยาก
- สำหรับการกรอกข้อมูลที่มีตัวเลือกจำนวนมาก เช่น การค้นหาสถานที่ ชื่อ หรือหมวดหมู่ต่าง ๆ
ก่อนไปสู่เนื้อหาการใช้งาน autocomplete widget เราจะมาลองสร้าง รูปแบบการค้นหารายการ
อย่างง่าย โดยไม่ใช้ autocomplete โดยมีหน้าค้นหา แล้วเมื่อผู้ใช้กรอกคำค้นหาใดๆได้ สามารถลบ
ได้ และหากกรอกข้อมูล ก็จะทำการแสดงรายการที่ตรงกับคำค้นหานั้นๆ เป็นรูปแบบคล้ายลิสรายการ
เช่นเดียวกับการใช้งาน autocomplete
เริ่มต้น เรามีไฟล์หน้าค้นหาชื่อว่า lib > screens > search.dart ดังนี้
ไฟล์ search.dart
import 'package:flutter/material.dart'; class Search extends StatefulWidget { static const routeName = '/search'; const Search({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _SearchState(); } } class _SearchState extends State<Search> { // สร้าง controller สำหรับควบคุม textfield final TextEditingController _searchController = TextEditingController(); // สร้าง focusnode สำหรับใช้งานร่วมกับแป้นพิมพ์ final FocusNode _focusNode = FocusNode(); // กำหนดข้อมูลตัวอย่างแบบตายตัว เพื่อแสดงแนวทาง static const List<String> suggestions = <String>[ 'apple', 'banana', 'cherry', 'date', 'fig', 'grape', 'kiwi', ]; @override void dispose() { _searchController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { print("debug: build"); return Scaffold( appBar: AppBar( elevation: 1, title: TextField( style: TextStyle( fontSize: 20, ), controller: _searchController, focusNode: _focusNode, decoration: InputDecoration( hintText: 'ค้นหา', hintStyle: TextStyle(color: Colors.grey), border: InputBorder.none, suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: Icon(Icons.clear, color: Colors.grey), onPressed: () { setState(() { _searchController.clear(); }); }, ) : null, ), onChanged: (String value) { setState(() {}); }, ), actions: [ IconButton( icon: Icon(Icons.search, color: Colors.black), onPressed: () {}, ), ], ), body: Stack( children: [ Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Search Screen'), ], ), ), if (_searchController.text.isNotEmpty) // แสดงข้อมูลมูล Positioned( top: 0, left: 0, right: 0, child: Material( elevation: 4.0, child: ListView( shrinkWrap: true, children: suggestions .where((option) => option .toLowerCase() .contains(_searchController.text.toLowerCase())) .map((String option) => ListTile( title: Text(option), onTap: () { setState(() { _searchController.text = option; }); _focusNode.unfocus(); print('debug: Selected: $option'); }, )) .take(10).toList(), ), ), ), ], ), ); } }
ผลลัพธ์ที่ได้
ตัวอย่างโค้ดข้างต้น ถ้าเรากรอกตัว a ซึ่งเป็นอักษรหนึ่งที่ตรงกับลิสรายการเริ่มต้นที่เรากำหนด ก็จะ
แสดงรายการทั้งหมดให้เราเลือก โดยเป็นรายการที่มีตัว a อยู่ในข้อความรายการนั้น และเรากำหนดว่า
ให้แสดงหรือดึงมามากสุดไม่เกิน 10 รายการ ในตัวอย่างมีที่ตรงทั้งหมด 4 รายการก็นำมาแสดงทั้งหมด
และเมื่อเราเลือกรายการใด ก็จะส่งค่ารายการนั้นไปใช้งานต่อ
ต่อไปเราจะปรับมาใช้งาน autocomplete widget เข้าไปแทนเพื่องสร้างรายการ ก็จะได้เป็น
ดังต่อไปนี้
ไฟล์ search.dart
import 'package:flutter/material.dart'; class Search extends StatefulWidget { static const routeName = '/search'; const Search({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _SearchState(); } } class _SearchState extends State<Search> { // สร้าง controller สำหรับควบคุม textfield final TextEditingController _searchController = TextEditingController(); // สร้าง focusnode สำหรับใช้งานร่วมกับแป้นพิมพ์ final FocusNode _focusNode = FocusNode(); // กำหนดข้อมูลตัวอย่างแบบตายตัว เพื่อแสดงแนวทาง static const List<String> suggestions = <String>[ 'apple', 'banana', 'cherry', 'date', 'fig', 'grape', 'kiwi', ]; @override void dispose() { _searchController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { print("debug: build"); return Scaffold( appBar: AppBar( elevation: 1, title: Autocomplete<String>( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { return const Iterable<String>.empty(); } return suggestions.where((String option) { return option .toLowerCase() .contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) { return TextField( style: TextStyle( fontSize: 20, ), controller: _searchController, focusNode: focusNode, decoration: InputDecoration( hintText: 'ค้นหา', hintStyle: TextStyle(color: Colors.grey), border: InputBorder.none, suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: Icon(Icons.clear, color: Colors.grey), onPressed: () { setState(() { _searchController.clear(); }); }, ) : null, ), onChanged: (value) { setState(() {}); }, ); }, onSelected: (String selection) { print('debug: Selected: $selection'); setState(() { _searchController.text = selection; }); }, ), actions: [ IconButton( icon: Icon(Icons.search, color: Colors.black), onPressed: () {}, ), ], ), body: Stack( children: [ Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Search Screen'), ], ), ), if (_searchController.text.isNotEmpty) // แสดงข้อมูลมูล Positioned( top: 0, left: 0, right: 0, child: Material( elevation: 4.0, child: ListView( shrinkWrap: true, children: suggestions .where((option) => option .toLowerCase() .contains(_searchController.text.toLowerCase())) .map((String option) => ListTile( title: Text(option), onTap: () { setState(() { _searchController.text = option; }); _focusNode.unfocus(); print('debug: Selected: $option'); }, )) .take(10) .toList(), ), ), ), ], ), ); } }
ผลลัพธ์ที่ได้
จากโค้ดตัวอย่าง ตอนนี้ เรานำตัว autocomplete widget มาใช้งานแล้ว โดยอาศัยค่าข้อมูลที่เป็น
ค่า suggest จากการกำหนดตัวแปรค่าที่ตายตัว ซึ่งกรณีที่เราไม่ได้ดึงข้อมูลจากส่วนอื่น และเป็นค่า
ที่จำเพาะอยู่แล้ว และไม่เกิน 100 รายการ เราสามารถกำหนดค่าในลักษณะนี้ได้ ในที่นี้ใช้รูปแบบนี้
เป็นตัวอย่าง
// กำหนดข้อมูลตัวอย่างแบบตายตัว เพื่อแสดงแนวทาง static const List<String> suggestions = <String>[ 'apple', 'banana', 'cherry', 'date', 'fig', 'grape', 'kiwi', ];
ส่วนของตัวแปร suggestions หรือตัวเลือก option รายการข้อมูล จะถูกนำมาสร้างเป็นลิสราย
การให้เราเลือกผ่านส่วนจัดการ optionsBuilder
optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { return const Iterable<String>.empty(); } return suggestions.where((String option) { return option .toLowerCase() .contains(textEditingValue.text.toLowerCase()); }); },
ถ้าไม่มีการกรอกข้อมูลเพื่อค้นหา ก็จะไม่แสดงค่าอะไรออกมาและใช้รูปแบบเป็น
return const Iterable<String>.empty();
แต่ถ้ามีการพิมพ์ข้อมูล เช่นตัวอย่าง พิมพ์ a ก็จะทำงานในส่วนของการค้นหาข้อมูลใน suggestions
โดยใช้ where ค่าที่กรอกเข้ามาค้นหาหรือกรองข้อมูล แล้วส่งออกกลับมาเป็นลิสรายการหรือ option
รายการข้อมูลที่มีข้อความที่กรอกอยู่ในชุดข้อมูล ในตัวอย่างมีการใช้คำสั่งเวลาแสดงออกมาให้ใช้
เป็นตัวพิมพ์เล็กด้วย
// คืนค่า options สำหรับนำไปสร้างลิสรายการ dropdown return suggestions.where((String option) { return option .toLowerCase() .contains(textEditingValue.text.toLowerCase()); });
โดยทั่วไปแล้ว ถ้าเราใช้งาน autocomplete widget ค่าจากส่วนนี้จะถูกนำไปสร้างเป็น dropdown
ให้เราโดยอัตโนมัติ แต่กรณีนี้ เราทำการสร้างลิสรายการขึ้นมาเอง และใช้ Stack ในการจัดเรียงให็แสดง
ซ้อนอยู่ด้านบนสุดของหน้าปัจจุบัน
ทั้งสองวิธีได้ผลลัพธ์ออกมาเหมือนๆ กัน แต่มีรูปแบบการใช้งานต่างกัน เราสามารถนำไปประยุกต์ได้
ซึ่งจริงๆ แล้วในตัวอย่างที่สองที่ใช้เป็น autocomplete widget หากเราไม่กำหนด Dropdown
แบบแสดงเอง และใช้ของส่วนที่ autocomplete มีมาให้ จะพบว่าตำแหน่งของ dropdown จะอ้างอิง
ตามตำแหน่งของ TextField ซึ่งจะอยู่ในส่วนของ title ของ appbar การแสดงผลก็จะเป้นในลักษณะ
ดังรูปด้านล่าง
อย่างไรก็ดี หากเราไม่ต้องการปรับให้ยุ่งยาก และอยากใช้งาน autocomplete อย่างเดียวก็สามารถ
ทำการย้อยส่วนของช่องค้นหามาไว้ในพื้นที่ส่วนของ body ได้ ดังนี้
ไฟล์ search.dart
import 'package:flutter/material.dart'; class Search extends StatefulWidget { static const routeName = '/search'; const Search({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _SearchState(); } } class _SearchState extends State<Search> { // สร้าง controller สำหรับควบคุม textfield final TextEditingController _searchController = TextEditingController(); // สร้าง focusnode สำหรับใช้งานร่วมกับแป้นพิมพ์ final FocusNode _focusNode = FocusNode(); // กำหนดข้อมูลตัวอย่างแบบตายตัว เพื่อแสดงแนวทาง static const List<String> suggestions = <String>[ 'apple', 'banana', 'cherry', 'date', 'fig', 'grape', 'kiwi', ]; @override void dispose() { _searchController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { print("debug: build"); return Scaffold( appBar: AppBar( title: Text('Search'), actions: [ IconButton( icon: Icon(Icons.search, color: Colors.black), onPressed: () {}, ), ], ), body: Stack( children: [ Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Search Screen'), ], ), ), Autocomplete<String>( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { return const Iterable<String>.empty(); } return suggestions.where((String option) { return option .toLowerCase() .contains(textEditingValue.text.toLowerCase()); }); }, fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) { return Padding( padding: const EdgeInsets.all(8.0), child: TextField( style: TextStyle( fontSize: 20, ), controller: textEditingController, focusNode: focusNode, decoration: InputDecoration( hintText: 'ค้นหา', hintStyle: const TextStyle(color: Colors.grey), enabledBorder: const UnderlineInputBorder( borderSide: BorderSide( color: Colors.green, width: 2.0, ), ), suffixIcon: textEditingController.text.isNotEmpty ? IconButton( icon: Icon(Icons.clear, color: Colors.grey), onPressed: () { setState(() { textEditingController.clear(); }); }, ) : null, ), onChanged: (value) { setState(() {}); }, ), ); }, onSelected: (String selection) { print('debug: Selected: $selection'); setState(() { _searchController.text = selection; }); }, ), ], ), ); } }
ผลลัพธ์ที่ได้
ดึงรายการ Autocomplete จาก API
โค้ดตัวอย่างสุดท้าย เราจะนำแนวทางการใช้งาน autocomplete ข้างต้นที่ได้กล่าวไป มาประยุกต์
ใช้งานกับเนื้อหาก่อนหน้าเกี่ยวกับ การแสดงรายการสินค้าจากบทความ
การใช้งาน Shimmer effect แสดงการรอโหลดข้อมูลใน Flutter http://niik.in/1111
โดยเราจะทำการเพิ่ม ส่วนของการค้นหาไว้ด้านบน โดยเมื่อทำการค้นหาก็จะไปนำข้อมูลสินค้าทั้งหมด
มาใช้สำหรับตัวเลือก ทำให้เราเลือกไปยังสินค้าตามชื่อที่ต้องการจากการค้นหา ซึ่งชื่อสินค้าก็เป็นค่าที่
ได้มาจาก api ที่ไปดึงมาใช้งานแต่แรก โค้ดนี้จะไม่ได้อธิบายอะไรมาก เป็นการปรับเปลี่ยนจากเดิมเป็น
List<String> แบบตายตัว มาเป็น List<Product> แทน
ไฟล์ product.dart
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 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import '../models/product_model.dart'; import '../shimmers/product_shimmer.dart'; class Products extends StatefulWidget { static const routeName = '/product'; const Products({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ProductsState(); } } class _ProductsState extends State<Products> { // สร้าง controller สำหรับควบคุม textfield final TextEditingController _searchController = TextEditingController(); // สร้าง focusnode สำหรับใช้งานร่วมกับแป้นพิมพ์ final FocusNode _focusNode = FocusNode(); // กำหนดตัวแปรสำหรับใช้ใน autocomplete late List<Product> products; // สร้างตัวแปรที่สามารถแจ้งเตือนการเปลี่ยนแปลงค่า 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( elevation: 1, title: Autocomplete<Product>( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { return const Iterable<Product>.empty(); } return products.where((Product product) { return product.title .toLowerCase() .contains(textEditingValue.text.toLowerCase()); }); }, displayStringForOption: (Product product) => product.title, fieldViewBuilder: (BuildContext context, TextEditingController textEditingController, FocusNode focusNode, VoidCallback onFieldSubmitted) { return TextField( style: TextStyle( fontSize: 20, ), controller: _searchController, focusNode: focusNode, decoration: InputDecoration( hintText: 'ค้นหา', hintStyle: TextStyle(color: Colors.grey), border: InputBorder.none, suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: Icon(Icons.clear, color: Colors.grey), onPressed: () { setState(() { _searchController.clear(); }); }, ) : null, ), onChanged: (value) { setState(() {}); }, ); }, onSelected: (Product selection) { print('debug: Selected: ${selection.title}'); setState(() { _searchController.text = selection.title; }); }, ), actions: [ IconButton( icon: Icon(Icons.search, color: Colors.black), onPressed: () {}, ), /* IconButton( onPressed: () async { if (!_isLoading && _visible.value == false) { _refresh(); } }, icon: const Icon(Icons.refresh_outlined), ) */ ], ), body: Stack( children: [ ListView( padding: const EdgeInsets.all(0.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(); // นำรายการที่ดึงจาก api มากำหนดใช้ในตัวแปร products สำหรับ autocomplete products = items; // แสดงแค่ 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(); } }, // ใช้งาน GridView child: GridView.builder( controller: _scrollController, padding: const EdgeInsets.all(5.0), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, // Number of columns crossAxisSpacing: 0.0, mainAxisSpacing: 0.0, childAspectRatio: 3 / 3.8, // Adjust to control the size ratio ), itemCount: items.length, itemBuilder: (context, index) { Product product = items[index]; Widget card; // สร้างเป็นตัวแปร card = Card( child: Padding( padding: const EdgeInsets.all(3.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Align( child: CachedNetworkImage( imageUrl: product.image, height: 150, fit: BoxFit.contain, placeholder: (context, url) => const Center( child: SizedBox( width: 40.0, height: 40.0, child: CircularProgressIndicator(), ), ), errorWidget: (context, url, error) => const Icon(Icons.error), ), ), Padding( padding: const EdgeInsets.all(3.0), child: Text( product.title, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Padding( padding: const EdgeInsets.all(3.0), child: Text( 'Price: \$ ${product.price}'), ), ], ), )); return card; }, ), // ใช้งาน GridView ) : const Center( child: Text('No items')), // กรณีไม่มีรายการ ), ], ); } else if (snapshot.hasError) { return Center(child: Text('${snapshot.error}')); } // return const Center(child: CircularProgressIndicator()); return const ShimmerLoadingGrid(); }, ), ], ), if (_searchController.text.isNotEmpty) // แสดงข้อมูลมูล Positioned( top: 0, left: 0, right: 0, child: Material( elevation: 4.0, child: ListView( shrinkWrap: true, children: products .where((product) => product.title .toLowerCase() .contains(_searchController.text.toLowerCase())) .map((Product product) => ListTile( title: Text(product.title), subtitle: Text( 'Price: \$${product.price.toString()}'), onTap: () { setState(() { _searchController.text = product.title; }); _focusNode.unfocus(); print('debug: Selected Product: ${product.title}'); }, )) .take(10) // Limits suggestions to 10 products .toList(), ), ), ), ], ), 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(); }
ผลลัพธ์ที่ได้
สำหรับรูปแบบลิสรายการข้างต้นเราแสดงด้วย ListTile เพื่อให้มองภาพรวมออก สามารถนำไปประยุกต์
ตัดส่วนที่ไม่ต้องการออกได้ หรือปรับเป็นรูปแบบอื่น ในตัวอย่างผลลัพธ์ เมื่อเราทำการค้นหาสินค้าด้วย
คำว่า ssd ก็จะแสดงรายการตัวเลือกที่ตรงให้เรากดเข้าไป ในที่นี้จะแสดงเท่านั้น ไม่ได้ให้ทำงานใดๆ เมื่อ
กดเลือกเข้าไป สามารถนำปรับประยุกต์ตามรูปแบบตัวเองได้ตามต้องการ