ประยุกต์การค้นหา ด้วย Autocomplete class ใน flutter

บทความใหม่ ไม่กี่เดือนก่อน โดย Ninenik Narkdee
flutter autocomplete

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

ดูแล้ว 291 ครั้ง


เนื้อหาในตอนต่อไปนี้ เราจะมีดูเกี่ยวกับการใช้งาน 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 ก็จะแสดงรายการตัวเลือกที่ตรงให้เรากดเข้าไป ในที่นี้จะแสดงเท่านั้น ไม่ได้ให้ทำงานใดๆ เมื่อ
กดเลือกเข้าไป สามารถนำปรับประยุกต์ตามรูปแบบตัวเองได้ตามต้องการ


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 12-09-2024


ดาวน์โหลดโค้ดตัวอย่าง สามารถนำไปประยุกต์ หรือ run ทดสอบได้

http://niik.in/download/flutter/demo_041_12092024_source.rar


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



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



ทบทวนบทความที่แล้ว









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






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



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




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





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

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


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


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







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