การ Cache ข้อมูลเพิ่มความเร็วสำหรับการโหลดข้อมูล Server

บทความใหม่ ยังไม่ถึงปี โดย Ninenik Narkdee
intl http cache path provider cached network image

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

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


เนื้อหาตอนต่อไปนี้ เราจะต่อยอดจากบทความที่ผ่านมา
เกี่ยวกับการจัดการข้อมูล future ซึ่งมีลำดับขั้นตอนเบื้องต้น
ตามที่ได้อธิบายไปแล้ว อยากไรก็ตาม การนำไปใช้งานจริงๆ เราจำเป็น
ต้องจัดการข้อมูลมากยิ่งขึ้น และอาจจะเจอปัญหาเกี่ยวกับโครงสร้างของ
ui ในขั้นตอนการจัดวางรูปแบบการแสดงผล เหล่านี้เราต้องฝึกบ่อยๆ 
จึงจะสามารถรวบรวมแนวทางและหาวิธีแก้ป้ญหาต่างๆ ได้
 
ทบทวนกระบวนการโหลดข้อมูล Future และการใช้งาน ValueNotifier 
 

สิ่งที่เราจะทำและเรียนรู้ในบทความนี้

    - เราจะทำการดึงข้อมูลจริงๆ จาก server แต่ใช้เป็นข้อมูลจำลอง
    - ทำการ cache ข้อมูลเป็นไฟล์ให้สามารถใช้งานข้อมูลได้เร็วขึ้น
    - เข้าใจภาพรวมการจัดหารหน้าแสดงรายการมากขึ้น
 

ประยุกต์การแสดงข้อมูลจาก API ของ Server

    จริงๆแล้ว เนื้อหาเหล่านี้เรามีข้อมูลและตัวอย่างการแนะนำทั้งหมดในบทความที่ผ่านๆ มาแล้ว
อย่างในกรณีที่เราจะประยุกต์นี้ ก็คล้ายกับบทความ
 
จัดการข้อมูล Model และแนวทางการนำมาใช้งาน ใน Flutter http://niik.in/1041
การใช้งาน Path Provider และการเชียนอ่าน File ใน Flutter http://niik.in/1066
การใช้งาน Http ดึงข้อมูลจาก Server มาแสดงใน Flutter http://niik.in/1038
 
    อย่างไรก็ตาม ก่อนอื่นเลย ให้เราเตรียมส่วนของ package ต่างๆ ดังนี้ไว้ในโปรเจ็ค
 
  http: ^1.2.2
  path_provider: ^2.1.4
  cached_network_image: ^3.4.1
  intl: ^0.19.0
 
http สำหรับดึงข้อมูลจาก server
path_provider สำหรับจัดการเกี่ยวกับไฟล์ ที่เราจะสร้างเป็นไฟล์ cache
cached_network_image สำหรับ cache รูปที่เราแสดง
intl จัดการเกี่ยวกับวันที่และเวลา (อาจจะยังไมได้ใช้ในบทความนี้ แต่แอปส่วนใหญ่จะใช้งาน)
 
โค้ดตัวอย่างต่อไปนี้ เราพิมพ์เติมจากบทความตอนที่ผ่านมา คือเพิ่มส่วนของ Data Model ของ
ข้อมูลสินค้า ที่เราจะไปดึงจาก api มาใช้งาน ในโฟลเดอร์ lib > models > product_model.dart
ในท้ายบทความมีไฟล์ทั้งหมดให้ดาวน์โหลด
 
จากนั้นเรากำหนดในไฟล์ home.dart เป็นดังนี้
 

ไฟล์ home.dart

 
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
import 'dart:async';
import 'dart:convert';
 
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/product_model.dart';
 
class Home extends StatefulWidget {
  static const routeName = '/home';
 
  const Home({Key? key}) : super(key: key);
 
  @override
  State<StatefulWidget> createState() {
    return _HomeState();
  }
}
 
class _HomeState extends State<Home> {
  // สร้างตัวแปรที่สามารถแจ้งเตือนการเปลี่ยนแปลงค่า
  final ValueNotifier<bool> _visible = ValueNotifier<bool>(false);
  // กำนหดตัวแปรข้อมูล products
  Future<List<Product>> _products = Future.value([]);
  // ตัว ScrollController สำหรับจัดการการ scroll ใน ListView
  final ScrollController _scrollController = ScrollController();
 
  // จำลองใช้เป็นแบบฟังก์ชั่น ให้เสมือนดึงข้อมูลจาก server
  Future<String> fetchData() async {
    print("debug: do function");
    final response = await Future<String>.delayed(
      const Duration(seconds: 2),
      () {
        return 'Data Loaded \n${DateTime.now()}';
      },
    );
    return response;
  }
 
  Future<void> _refresh() async {
    _visible.value = true;
    setState(() {
      _products = fetchProduct();
    });
  }
 
  @override
  void initState() {
    print("debug: Init");
    super.initState();
    _products = fetchProduct();
  }
 
  @override
  void dispose() {
    _scrollController.dispose();
    _visible.dispose(); // Dispose the ValueNotifier
    super.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    print("debug: build");
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        actions: [
          IconButton(
            onPressed: () {
              _refresh();
            },
            icon: const Icon(Icons.refresh_outlined),
          )
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(8.0),
        children: [
          ValueListenableBuilder<bool>(
            valueListenable: _visible,
            builder: (context, visible, child) {
              return Visibility(
                visible: visible,
                child: const LinearProgressIndicator(
                  backgroundColor: Colors.white60,
                ),
              );
            },
          ),
          FutureBuilder<List<Product>>(
            // ชนิดของข้อมูล
            future: _products,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {}
              if (snapshot.connectionState == ConnectionState.done) {
                WidgetsBinding.instance.addPostFrameCallback((_) {
                  // Change state after the build is complete
                  _visible.value = false;
       
                  if (_scrollController.hasClients) {
                    //เช็คว่ามีตัว widget ที่ scroll ได้หรือไม่ ถ้ามี
                    // เลื่อน scroll มาด้านบนสุด
                    _scrollController.animateTo(0,
                        duration: Duration(milliseconds: 500),
                        curve: Curves.fastOutSlowIn);
                  }
                });
              }
              if (snapshot.hasData) {
                // แสดงทั้งหมด
                 final items = snapshot.data!.toList();
                // แสดงแค่ 10 รายการ
                // final items = snapshot.data!.take(10).toList();
       
                return Column(
                  children: [
                    Container(
                      // สร้างส่วน header ของลิสรายการ
                      padding: const EdgeInsets.all(5.0),
                      decoration: BoxDecoration(
                        color: Colors.orange.withAlpha(100),
                      ),
                      child: Row(
                        children: [
                          Text(
                              'Total ${items.length} items'), // แสดงจำนวนรายการ
                        ],
                      ),
                    ),
                    SizedBox(
                      // ปรับความสูงขางรายการทั้งหมด  การ ลบค่า เพื่อให้ข้อมูลแสดงเต็มพื้นที่
                      // หากมี appbar ควรลบ 100 ถ้าไม่มีควรลบ 200 หรือค่าอื่นๆ ตามเหมาะสม
                      height: MediaQuery.of(context).size.height - 200,
                      child: snapshot.data!.isNotEmpty // กำหนดเงื่อนไขตรงนี้
                          ? RefreshIndicator(
                              onRefresh: () async {
                                _refresh();
                              }, // Function to call when the user pulls to refresh
                              child: ListView.separated(
                                // กรณีมีรายการ แสดงปกติ
                                controller:
                                    _scrollController, // กำนหนด controller ที่จะใช้งานร่วม
                                itemCount: items.length,
                                itemBuilder: (context, index) {
                                  Product product = items[index];
       
                                  Widget card; // สร้างเป็นตัวแปร
                                  card = Card(
                                      margin: const EdgeInsets.all(
                                          5.0), // การเยื้องขอบ
                                      child: Column(
                                        children: [
                                          ListTile(
                                            leading: Image.network(
                                              product.image,
                                              width: 100.0,
                                            ),
                                            title: Text(product.title),
                                            subtitle: Text(
                                                'Price: \$ ${product.price}'),
                                            trailing: Icon(Icons.more_vert),
                                            onTap: () {},
                                          ),
                                        ],
                                      ));
                                  return card;
                                },
                                separatorBuilder:
                                    (BuildContext context, int index) =>
                                        const SizedBox(),
                              ),
                            )
                          : const Center(
                              child: Text('No items')), // กรณีไม่มีรายการ
                    ),
                  ],
                );
              } else if (snapshot.hasError) {
                return Center(child: Text('${snapshot.error}'));
              }
              return const Center(child: CircularProgressIndicator());
            },
          ),
        ],
      ),
      floatingActionButton: ValueListenableBuilder<bool>(
        valueListenable: _visible,
        builder: (context, visible, child) {
          return (visible == false)
              ? FloatingActionButton(
                  onPressed: () {
                    _refresh();
                  },
                  shape: const CircleBorder(),
                  child: const Icon(Icons.refresh),
                )
              : SizedBox.shrink();
        },
      ),
    );
  }
}
 
// สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Product
Future<List<Product>> fetchProduct() async {
  // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด
  final response = await http.get(Uri.parse(url));
 
  // เมื่อมีข้อมูลกลับมา
  if (response.statusCode == 200) {
    // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Product
    // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseProducts
    // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body
    return compute(parseProducts, response.body);
  } else {
    // กรณี error
    throw Exception('Failed to load product');
  }
}
 
// ฟังก์ชั่นแปลงข้อมูล JSON String data เป็น เป็นข้อมูล List<Product>
List<Product> parseProducts(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
  return parsed.map<Product>((json) => Product.fromJson(json)).toList();
}
 
ไฟล์เริ่มต้น ผลลัพธ์ที่ได้
 


 
กระบวนการทำงานจะคล้ายๆ กับบทความที่แล้ว คือ เปิดเข้ามา ทำการโหลดข้อมูลจาก server จากนั้น
นำมาแสดงลิสรายการสินค้า สามารถ refresh ได้หลากหลาย ทั้งกดที่ปุ่มที่กำหนด หรือ ปัดลงเพื่อ 
refresh มีการกำหนด ให้เลื่อนรายการกลับมาด้านบน หากโหลดข้อมูลใหม่เรียบร้อยแล้ว  เช่น สมมติ
ว่าเราเลื่อนไปอยู่รายการท้ายๆ แล้วทำการ refresh ข้อมูล เมื่อข้อมูลโหลดเสร็จ ก็จะทำการเลื่อน
หน้าจอมาด้านบน เราได้เพิ่มโค้ดของการกำหนดขนาดความสูงของ SizedBox widget ให้สัมพันธ์
กับขนาดความสูงของลิสรายการข้อมูลด้วย MediaQuery.of(context).size.height และเราได้
ทำการจับในส่วนของฟังก์ชั่นที่ดึงข้อมูลจาก server ออกมาข้างนอก State class เพื่อไม่ให้ส่วน
ของ state ดูซับซ้อนจัดการยากเกินไป อย่างไรก็ดี การแยกส่วนคำสั่งต่างๆ ก็จะทั้งข้อดีข้อเสียแตกต่าง
กันไป ขึ้นอยู่กับการประยุกต์ใช้งาน เช่น ถ้าฟังก์ชั่นดึงข้อมูลไม่ได้ซับซ้อนมาก เราก็สามารถเขียนไว้
ใน class state ได้เลยไม่ต้องแยกออกมาก็ได้ แต่ถ้าว่า คำสั่งการจัดการมีความซับซ้อนขึ้น การแยก
ออกมาอีกไฟล์หรือออกมาข้างนอกก็จะทำให้การจัดการทำได้ง่ายขึ้น
 
พยายามศึกษาโค้ดข้างต้นแต่ละบรรทัด ให้เข้าใจ เราจะได้สามารถนำไปประยุกต์ เมื่อใช้งานจริงๆ 
โค้ดตัวอย่างข้างต้น ยังไม่มีการสร้างไฟล์ cache แต่อย่างไร เป็นการดึงข้อมุลปกติทั่วไป นั่นคือเมื่อเข้า
มาหน้าแอปหน้านี้ก็จะทำการไปดึงข้อมูลจาก server ทุกๆ ครั้ง *ดาวน์โหลดไฟล์ตัวอย่างได้ที่ท้าย
บทความ
 
 

การ Cache ข้อมูลจาก Server มาเก็บไว้ในไฟล์เพื่อเรียกใช้งาน

    ต่อไปเรามาสู่ขั้นตอนการจัดการเกี่ยวกับการ cache ข้อมูล โดยจะมีเงื่อนไขการทำงานดึงนี้คือ 
ทุกครั้งที่ผู้ใช้ทำการ refresh ข้อมูล เราจะทำการตรวจสอบไฟล์ cache ที่เครืองการว่ามือหรือไม่
ถ้ามีเราจะใช้ไฟล์ cache ที่เครื่องมาแสดงแทนการไปโหลดที่ฝั่ง server ขั้นต้นเป็นแบบนี้ก่อน ดังนั้น
ส่วนที่เราจะดูก็คือฟังก์ชั่น การโหลดข้อมูล ในที่นี้คือ
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Product
Future<List<Product>> fetchProduct() async {
  // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด
  final response = await http.get(Uri.parse(url));
 
  // เมื่อมีข้อมูลกลับมา
  if (response.statusCode == 200) {
    // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Product
    // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseProducts
    // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body
    return compute(parseProducts, response.body);
  } else {
    // กรณี error
    throw Exception('Failed to load product');
  }
}
 
เนื้อหาเกี่ยวกับการจัดการไฟล์ เขียนไฟล์ อ่านไฟล์มีรายละเอียดแล้วที่บทความ http://niik.in/1066
เราจะปรับเป็นดังนี้
 
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
// สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล Future ประเภท List ของ Product
Future<List<Product>> fetchProduct() async {
  String _currentPath = ''; // เก็บ path ปัจจุบัน
  final appDocumentsDirectory = await getApplicationDocumentsDirectory();
 
  _currentPath = appDocumentsDirectory.path;
 
  String filename = "product_cache.json";
  String readFile = "$_currentPath/$filename";
 
  String _jsonData = '';
  final _file = File(readFile);
  final isExits = await _file.exists();
 
  try {
    if (isExits) {
      print("debug: read from file");
      _jsonData = await _file.readAsString();
      return compute(parseProducts, _jsonData);
    } else {
      // ทำการดึงข้อมูลจาก server ตาม url ที่กำหนด
      String url = 'https://fakestoreapi.com/products';
      final response = await http.get(Uri.parse(url));
 
      // เมื่อมีข้อมูลกลับมา
      if (response.statusCode == 200) {
        print("debug: load form server");
        final myfile = _file;
        final isExits = await myfile.exists(); // เช็คว่ามีไฟล์หรือไม่
        if (!isExits) {
          // ถ้ายังไม่มีไฟล์
          try {
            await myfile.writeAsString(response.body);
          } catch (e) {
            throw Exception('error: ${e}');
          }
        } else {
          try {
            await myfile.writeAsString(response.body);
          } catch (e) {
            throw Exception('error: ${e}');
          }
        }
        // ส่งข้อมูลที่เป็น JSON String data ไปทำการแปลง เป็นข้อมูล List<Product
        // โดยใช้คำสั่ง compute ทำงานเบื้องหลัง เรียกใช้ฟังก์ชั่นชื่อ parseProducts
        // ส่งข้อมูล JSON String data ผ่านตัวแปร response.body
        return compute(parseProducts, response.body);
      } else {
        // กรณี error
        throw Exception('Failed to load product');
      }
    }
  } catch (e) {
    throw Exception('error: ${e}');
  }
}
 
เมื่อมีการใช้งานในลักษณะข้างต้น เราจะต้อง import package ต่างๆ ที่จำเป็น เช่น ในตัวอย่างนี้
 
1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:async';
import 'dart:convert';
import 'dart:io';
 
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
// import 'package:intl/intl.dart'; // จัดรูปแบบวันทีและเวลา http://niik.in/1047
import 'package:cached_network_image/cached_network_image.dart';
import 'package:path_provider/path_provider.dart';
 
import '../models/product_model.dart';
 
กลับมาที่ส่วนของโค้ดกันต่อ จริงๆ ก็ไม่ซับซ้อนอะไร เริ่มต้นเรากำหนดชื่อไฟล์สำหรับเป็นไฟล์ cache
ชื่อว่า product_cache.json ซึ่งในการใช้งานครั้งแรก ไฟล์นี้จะยังไม่ถูกสร้าง ดังนั้นจึงเป็นการไป
โหลดข้อมูลจาก server จากนั้น ทำการสร้างไฟล์ แล้วนำข้อมูล ที่โหลดจาก server มาบันทึก
ลงในไฟล์ cache ดังกล่าว เมื่อกลับมาครั้งที่สอง ก็จะเป็นการไปโหลดข้อมุลจากไฟล์แทนการไปโหลด
ที่ server โดยตรง ทำให้ในการโหลดข้อมูล ก็จะทำงานเร็วขึ้น
 
แต่สิ่งต่อมาที่เราต้องพิจารณาต่อก็คือ แล้วการโหลดข้อมูลครั้งที่สองเป็นต้นไป เป็นการไปอ่านจากไฟล์
ตลอดแบบนี้ข้อมูลจะอัพเดทหรือไม่ เพราะข้อมูลอาจจะเป็นข้อมูลเก่า เราจะแก้ไขยังไง วิธีต่อไปก็คือ เรา
จะกำหนดให้ หากเป็นการโหลดข้อมูลปกติ จะใช้การเรียกจากไฟล์ แต่ถ้าทำการเรียกใช้ฟังก์ชั่น refresh
จะให้ทำการไปโหลดที่ฝั่ง server แล้วทำการเขียนข้อมูลใหม่ลงไฟล์ เพื่อให้เป็นข้อมูลอัพเดท ดังนั้นใน
ขั้นตอนการ refresh เราจะต้องมีการกำหนด parameter สำหรับให้ไปโหลดจาก server เป็นดังนี้
 
ในฟังก์ชั่น fetchProduct จะแก้ไข 2 จุดคือ 
 
1
2
3
4
5
// กำหนด parameter สำหรับการโหลดค่าใหม่เข้าไป ชื่อว่า reload
Future<List<Product>> fetchProduct({reload}) async {
 
// และส่วนของการอ่านจากไฟล์ จะอ่านจากไฟล์กรณีไม่ได้กำหนด reload เข้ามา
if(isExits && reload==null){
 
ต่อไปในส่วนของฟังก์ชั่น refresh
 
1
2
3
4
5
6
7
8
9
// เรากำหนด argument เพิ่ม reload มีค่าเท่ากับ true เข้าไป
// นั่นคือ ทุกครั้งที่กด refresh ก็จะทำการไปโหลดข้อมูลใหม่มาแสดง และบันทึก
// cache ข้อมูลล่าสุดไว้
  Future<void> _refresh() async {
    _visible.value = true;
    setState(() {
      _products = fetchProduct(reload: true);
    });
  }
 
ต่อไป เราจัดการส่วนของการแสดงรูปภาพต่อ เดิมจะเป็น
 
1
2
3
4
5
6
7
8
9
10
11
ListTile(
    leading: Image.network(
      product.image,
      width: 100.0,
    ),
    title: Text(product.title),
    subtitle: Text(
        'Price: \$ ${product.price}'),
    trailing: Icon(Icons.more_vert),
    onTap: () {},
  ),
 
เราใช้งาน cached_network_image package จัดการเป็นดังนี้
 
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
ListTile(
   leading: CachedNetworkImage(
     imageUrl: product.image,
     width: 100.0,
     placeholder: (context, url) =>
         Center(
       child: SizedBox(
         // Adjust the size as needed
         width: 40.0,
         height: 40.0,
         child:
             CircularProgressIndicator(), // Show loading indicator
       ),
     ),
     errorWidget: (context, url,
             error) =>
         Icon(Icons
             .error), // Show error icon if loading fails
   ),
   title: Text(product.title),
   subtitle: Text(
       'Price: \$ ${product.price}'),
   trailing: Icon(Icons.more_vert),
   onTap: () {},
 )
 

ไฟล์ home.dart รองรับการ cache

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


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


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

ตัวอย่างการใช้งาน  ยังไม่ประยุกต์การ cache ไฟล์จาก server

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


   เพิ่มเติมเนื้อหา ครั้งที่ 2 วันที่ 24-08-2024


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

ตัวอย่างการใช้งาน  ประยุกต์การ cache ไฟล์จาก server

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


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



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



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









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






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

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

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

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



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




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











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