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

บทความใหม่ ยังไม่ถึงปี โดย Ninenik Narkdee
autocomplete flutter

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

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


เนื้อหาในตอนต่อไปนี้ เราจะมีดูเกี่ยวกับการใช้งาน 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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 รายการ เราสามารถกำหนดค่าในลักษณะนี้ได้ ในที่นี้ใช้รูปแบบนี้
เป็นตัวอย่าง
 
1
2
3
4
5
6
7
8
9
10
// กำหนดข้อมูลตัวอย่างแบบตายตัว เพื่อแสดงแนวทาง
static const List<String> suggestions = <String>[
  'apple',
  'banana',
  'cherry',
  'date',
  'fig',
  'grape',
  'kiwi',
];
 
ส่วนของตัวแปร  suggestions หรือตัวเลือก option รายการข้อมูล จะถูกนำมาสร้างเป็นลิสราย
การให้เราเลือกผ่านส่วนจัดการ optionsBuilder
 
1
2
3
4
5
6
7
8
9
10
optionsBuilder: (TextEditingValue textEditingValue) {
  if (textEditingValue.text.isEmpty) {
    return const Iterable<String>.empty();
  }
  return suggestions.where((String option) {
    return option
        .toLowerCase()
        .contains(textEditingValue.text.toLowerCase());
  });
},
 
ถ้าไม่มีการกรอกข้อมูลเพื่อค้นหา ก็จะไม่แสดงค่าอะไรออกมาและใช้รูปแบบเป็น
 
1
return const Iterable<String>.empty();

 
แต่ถ้ามีการพิมพ์ข้อมูล เช่นตัวอย่าง พิมพ์ a ก็จะทำงานในส่วนของการค้นหาข้อมูลใน suggestions
โดยใช้ where ค่าที่กรอกเข้ามาค้นหาหรือกรองข้อมูล แล้วส่งออกกลับมาเป็นลิสรายการหรือ option
รายการข้อมูลที่มีข้อความที่กรอกอยู่ในชุดข้อมูล ในตัวอย่างมีการใช้คำสั่งเวลาแสดงออกมาให้ใช้
เป็นตัวพิมพ์เล็กด้วย
 
1
2
3
4
5
6
// คืนค่า 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
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 สำหรับอ้างอิง











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