จัดการข้อมูล Model และแนวทางการนำมาใช้งาน ใน Flutter

เขียนเมื่อ 3 ปีก่อน โดย Ninenik Narkdee
dialogroute flutter dismissible singlechildscrollview data model pageroute

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

ดูแล้ว 8,720 ครั้ง




เนื้อหาตอนต่อไปนี้จะประยุกต์ต่อเนื่องจากตอนที่แล้ว เราจะลองใช้
ข้อมูลทดสอบที่สมจริงมากยิ่งขึ้น และแนะนำรูปแบบการนำข้อมูลไปใช้
งานอย่างมีรูปแบบ ทบทวนบทความตอนที่แล้วได้ที่
    การใช้งาน RefreshIndicator ปัดเพื่อรีเฟรชข้อมูล ใน Flutter http://niik.in/1040 
 
    *เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/961
 
 

การจัดการข้อมูล Data Model

    ในบทความที่ผ่านมา เราได้ใช้ข้อมูลทดสอบโดยดึงมาจาก server 
https://jsonplaceholder.typicode.com เป็นรูปแบบข้อมูลที่มีโครงสร้างอย่างง่าย
และไม่ซับซ้อนนัก รวมทั้งเป็นข้อมูลหลอกที่อาจจะสื่ออะไรได้ไม่มาก เราจะเปลี่ยนมาใช้
ข้อมูลทดสอบจากแหล่งใหม่ เป็นข้อมูลของสินค้า หรือ product โดยใช้ข้อมูลจากเว็บไซต์
https://fakestoreapi.com เราจะใช้ข้อมูลในส่วนของ product มีโครงสร้าง JSON String data
เป็นดังนี้
 
1
2
3
4
5
6
7
8
9
10
11
12
{
   "id":1,
   "title":"Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
   "price":109.95,
   "description":"Your perfect pack for everyday use and walks in the forest. ",
   "category":"men's clothing",
   "rating":{
      "rate":3.9,
      "count":120
   }
}
 
    สิ่งที่เราจะทำคือดึงรายการมาแสดงโดยใช้ ListView แล้วเมื่อกดเลือกที่รายการใดๆ เราจะแสดง
ข้อมูลนั้นในอีกหน้าใน 2 รูปแบบคือ แบบ DialogRoute และแบบ MaterialPageRoute หรือหน้า screen ใหม่
 

    เตรียม Data Model

    เราจะทำการแยกส่วนของ data model หรือรูปแบบข้อมูลมาไว้ในอีกไฟล์ อีกโฟลเดอร์ ให้เราสร้างโฟลเดอร์
ชื่อ models ไว้ในโฟลเดอร์ lib แล้วสร้างไฟล์ชื่อว่า product_model.dart และกำหนดเป็นดังนี้
 
    โครงสร้างไฟล์
 
 


 
 
    ไฟล์ product_model.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
// Data models
class Product {
  int id;
  String title;
  num price;
  String description;
  String category;
  String image;
  Rating? rating;
 
  Product({
    required this.id,
    required this.title,
    required this.price,
    required this.description,
    required this.category,
    required this.image,
    this.rating
  });
 
  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'],
      title: json['title'],
      price: json['price'],
      description: json['description'],
      category: json['category'],
      image: json['image'],
      rating: json['rating'] != null ? Rating.fromJson(json['rating']) : null
    );
  }
 
}
 
class Rating {
  num? rate;
  int? count;
 
  Rating({
    this.rate,
    this.count});
 
  factory Rating.fromJson(Map<String, dynamic> json) {
    return Rating(
      rate: json['rate'],
      count: json['count']
    );
  }
 
}
 
    เกี่ยวกับ Data Model อธิบายเสริมไว้ในเนื้อหาเสริมตอนท้ายของลิ้งค์ http://niik.in/1038
    ต่อไปเราจะกำหนดและดึงข้อมูลจาก fakestoreapi.com มาแสดงในหน้า 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
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 = '/';
    
    const Home({Key? key}) : super(key: key);
     
    @override
    State<StatefulWidget> createState() {
        return _HomeState();
    }
}
     
class _HomeState extends State<Home> {
   
    // กำนหดตัวแปรข้อมูล products
    late Future<List<Product>> products;
    // ตัว ScrollController สำหรับจัดการการ scroll ใน ListView
    final ScrollController _scrollController = ScrollController();
     
    @override
    void initState() {
      super.initState();
      products = fetchProduct();
    }    
   
    Future<void> _refresh() async {
      setState(() {
       products = fetchProduct();
      });
    }
  
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('Home'),
            ),
            body: Center(
              child: FutureBuilder<List<Product>>( // ชนิดของข้อมูล
                future: products, // ข้อมูล Future
                builder: (context, snapshot) {
                  // มีข้อมูล และต้องเป็น done ถึงจะแสดงข้อมูล ถ้าไม่ใช่ ก็แสดงตัว loading
                  if (snapshot.hasData) {
                      bool _visible = false; // กำหนดสถานะการแสดง หรือมองเห็น เป็นไม่แสดง
                      if(snapshot.connectionState == ConnectionState.waiting){ // เมื่อกำลังรอข้อมูล
                        _visible = true; // เปลี่ยนสถานะเป็นแสดง
                      }
                      if(_scrollController.hasClients){ //เช็คว่ามีตัว widget ที่ scroll ได้หรือไม่ ถ้ามี
                        // เลื่อน scroll มาด้านบนสุด
                      _scrollController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
                      }
                      return Column(
                          children: [
                            Visibility(
                              child: const LinearProgressIndicator(),
                              visible: _visible,
                            ),
                            Container( // สร้างส่วน header ของลิสรายการ
                              padding: const EdgeInsets.all(5.0),
                              decoration: BoxDecoration(
                                color: Colors.orange.withAlpha(100),
                              ),                 
                              child: Row(                 
                                children: [
                                  Text('Total ${snapshot.data!.length} items'), // แสดงจำนวนรายการ
                                ],
                              ),
                            ),
                            Expanded( // ส่วนของลิสรายการ
                                child: snapshot.data!.isNotEmpty // กำหนดเงื่อนไขตรงนี้
                                ? RefreshIndicator(
                                  onRefresh: _refresh,
                                  child: ListView.separated( // กรณีมีรายการ แสดงปกติ
                                        controller: _scrollController, // กำนหนด controller ที่จะใช้งานร่วม
                                        itemCount: snapshot.data!.length,
                                        itemBuilder: (context, index) {
                                          Product product = snapshot.data![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) { // กรณี error
                    return Text('${snapshot.error}');
                  }
                  // กรณีสถานะเป็น waiting ยังไม่มีข้อมูล แสดงตัว loading
                 return const RefreshProgressIndicator();
                },
              ), 
            ),         
        );
    }  
   
}
  
// สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล 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();
}
  
    ผลลัพธ์ที่ได้
 
 


 
 
    เราทำการกำหนด Data Model โดยแยกไปไว้ในอีกไฟล์ แล้ว import เข้ามาใช้งาน
 
1
import '../models/product_model.dart';
 
    เปลี่ยนชนิดของข้อมูล เป็น List<Product> ทั้งหมด
    เปลี่ยนการเรียกข้อมูลเป็น url ของ server fakestoreapi
 
 
    จัดรูปแบบการแสดงด้วย ListTile ดังนี้
 
1
2
3
4
5
6
7
8
9
ListTile(
  leading: Image.network(product.image,width: 100.0,),
  title: Text(product.title),
  subtitle: Text('Price: \$ ${product.price}'),
  trailing: Icon(Icons.more_vert),
  onTap: (){
                                                 
  },
),
 
    เราจะสนใจเฉพาะในส่วนนี้ รายการสินค้า เรากำหนดให้แสดงรูป ชื่อสินค้า และราคา ดังรูป 
และกำหนดให้ รองรับการทำงานเมื่อกดเลือกสินค้าใดๆ ซึ่งเรายังไม่ได้เพิ่มคำสั่งการทำงานลงไป
ตอนนี้เรามีหน้ารายการสินค้าพร้อมแล้วสำหรับขั้นตอนต่อไป
 
 

แสดงรายละเอียดสินค้าด้วย PageRoute

    PageRoute หรือ route หลักเหมือนหน้าเพจต่างๆ ที่เราไว้แสดง วิธีแสดงรายละเอียดสินค้าวิธีแรก
ที่จะนำเสนอ ก่อนอื่นเราต้องทำการสร้างไฟล์หน้านี้ขึ้นมา ในที่นี้ใช้ชื่อว่า product.dart ไว้ในโฟลเดอร์ 
screen ตามโครงสร้าง project ดังนี้
 
 


 
 

    กำหนดไฟล์ 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
import 'package:flutter/material.dart';
import '../models/product_model.dart';
  
class Products extends StatefulWidget {
    static const routeName = '/producs';
  
    const Products({Key? key}) : super(key: key);
     
    @override
    State<StatefulWidget> createState() {
        return _ProductsState();
    }
}
  
class _ProductsState extends State<Products> {
  
    @override
    Widget build(BuildContext context) {
        // รับค่า arguments ที่ส่งมาใช้งาน
        final product = ModalRoute.of(context)!.settings.arguments as Product; 
 
        return Scaffold(
                appBar: AppBar( // เราใช้ appbar แต่จะให้แสดงแต่ปุ่มปิด
                    leading: IconButton( // ใช้ไอคอนปุ่ม
                      onPressed: (){ // เมื่อกด
                        Navigator.of(context).pop(); //ปิดหน้านี้
                      },
                      icon: Icon(Icons.close,color: Colors.black),
                    ),
                    backgroundColor: Colors.white,
                    elevation: 0.0,
                ),
                body: Container(
                    child: SingleChildScrollView( //ใช้งาน widget นี้เพื่อป้องกัน error พื้นที่เกินขอบเขต
                      child: Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Column(
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: <Widget>[
                              Image.network(product.image), // แสดงรูป
                              SizedBox(height: 10.0),
                              Text(product.title, // แสดงชื่อสินค้า
                                style: Theme.of(context).textTheme.headlineMedium,
                              ),
                              SizedBox(height: 5.0),
                              Text('Price: \$ ${product.price}'), // แสดงราคา
                              SizedBox(height: 10.0),
                              Text('Price: ${product.description}'), // แสดงรายละเอียด
                            ],
                        ),
                      ),
                    )
                ),
            );
    }
}
 
    โค้ดในหน้าแสดงรายละเอียดไม่มีอะไรซับซ้อน คำอธิบายแสดงในโค้ด จะขออธิบายเพิ่มเติม
ตัว Object ที่เราเอามาแสดงในหน้านี้ก็คือ Product object ดังนั้น ก็ต้อง import Data Model 
เข้ามาใช้งาน
 
1
import '../models/product_model.dart';
 
เราจะทำการส่งข้อมูล product เข้ามาในหน้านี้ จากนั้นก็นำมาแสดง เกี่ยวกับการรับค่าเมื่อใช้งาน
Navigator และ Routing อธิบายละเอียดไว้แล้วในบทความตามลิ้งค์ด้านล่าง
    การใช้งาน Navigator และ Routing ใน Flutter เบื้องต้น http://niik.in/958
 
1
2
// รับค่า arguments ที่ส่งมาใช้งาน
final product = ModalRoute.of(context)!.settings.arguments as Product;
 
    เราทำการรับค่าจาก arguments ที่ส่งมา แล้วเก็บไว้ในตัวแปร product เพื่อใช้งาน
 
    ในโค้ดจะเห็นว่าเรามีการใช้งาน SingleChildScrollView() widget นี้จะช่วยป้องกันการ error ที่เกิดจากการ
ใช้พื้นที่เกินขอบเขตที่กำหนดเริ่มต้น เข้าใจอย่างง่าย เช่น เรามีกล่องสูง 100 เวลาแสดงข้อมูล ถ้าเรามีรูป หลาย
ขนาด 50 60 110 แบบนี้ ถ้ารูปที่มีขนาด 60 ถูกแสดงในกล่องที่สูง 100 ก็แสดงได้ปกติไม่มีปัญหา แต่ถ้าพอเป็น
รูปที่ดึงมาแสดงเป็นขนาด 110 ตัวพื้นที่เดิมที่มีแค่ 100 ก็จะไม่พอทำให้เกิด error เกี่ยวกับพื้นที่ขึ้น แบบนี้เป็นต้น
ตัว widget จะเป็นตัวที่จะสร้างให้ส่วนของเนื้อหานั้นสามารถเลื่อนได้ หรือมีพื้นที่ขยายรองรับตามเนื้อหาที่เกินเข้ามา
ได้นั่นเอง
 
    เมื่อเราเตรียมหน้า product สำหรับแสดงข้อมูลแบบ PageRoute เรียบร้อยแล้ว เราก็กำหนดการส่งค่าในส่วน
ของ ListTile ในไฟล์ home.dart เข้าไปเป็นดังนี้
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ListTile(
  leading: Image.network(product.image,width: 100.0,),
  title: Text(product.title),
  subtitle: Text('Price: \$ ${product.price}'),
  trailing: Icon(Icons.more_vert),
  onTap: (){
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => Products(), // กำหนด PageRoute
        settings: RouteSettings(
          arguments: product // ส่งค่าไปใน  arguments  
        ),
      ),
    );                                             
  },
),
 
    เราเพิ่มในส่วนของ Navigator.push() และกำหนดหน้า PageRoute ด้วย Products() อย่าลืม 
import ไฟล์ product.dart มาใช้งานในหน้า home.dart ด้วย
 
1
2
3
4
5
6
7
8
9
import 'dart:async';
import 'dart:convert';
   
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
 
import 'product.dart';
import '../models/product_model.dart';
 
    เราส่ง Product object ไปยังหน้า product ไปในค่า arguments ผ่านชื่อตัวแปร product
 
    ดูผลลัพธ์ที่ได้ จะเป็นดังนี้
 
 


 
 
 

แสดงรายละเอียดสินค้าด้วย DialogRoute

    รูปแบบ Dialog ก็จะเหมือน popup หรือ alert ถ้าตัว dialog ที่แสดงนั้นมีขนาดไม่เต็มพื้นที่ เราก็จะมอง
เห็นส่วนของหน้าที่เปิด dialog แสดงอยู่ด้านหลังม่านคลุมมืดสีจางๆ แต่สำหรับ DialogRoute จะเป็นแบบเต็ม
พื้นที่ที่เราสามารถนำ widget ต่างๆ มาแสดงได้ ซึ่งเราก็จะนำ Scaffold() มาแสดง และมากกว่านั้น เราจะมี
การใช้งาน Dismissible() widget ร่วมด้วย
    Dismissible() ก็คือ widget เข้าใจอย่างง่ายคือ widget ที่ให้เราสามารถกำหนดพื้นที่การละทิ้ง การยกเลิก
สิ่งที่กำลังแสดงอยู่ตรงหน้า สมมติเช่น ถ้าเป็น dialog alert ที่แสดงตรงกลาง แล้วรอบๆ ข้างเป็นส่วนม่านคลุมมืด
สีจางๆ ที่พอเรากดก็จะเป็นการปิด dialog นั้นไป นั่นแหละคือหน้าที่ที่เหมือนกับของ dismissible() widget
    เราจะสร้าง DialogRoute ด้วยรูปแบบของ static ฟังก์ชั่น ไม่ได้มีการสร้างหน้าหรือไฟล์ขึ้นมาใหม่
 
    ในไฟล์ home.dart  เราเพิ่ม static ฟังก์ชั่นเข้าไปชื่อ _viewProduct()
 
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
// สร้างฟังก์ชั่น ที่คืนค่าเป็น route ของ object ฟังก์ชั่นนี้ มี context และ product เป็น parameter
static Route<Object?> _viewProduct(BuildContext context, Product product) {
  return DialogRoute<void>(
          context: context,
          builder: (BuildContext context) => // ใช้ arrow aฟังก์ชั่น
            Dismissible( // คืนค่าเป็น dismissible widget
              direction: DismissDirection.vertical, // เมื่อปัดลงในแนวตั้ง
              key: const Key('key'), // ต้องกำหนด key ใช้ค่าตามนี้ได้เลย
              onDismissed: (_) => Navigator.of(context).pop(), // ปัดลงเพื่อปิด            
              child: Scaffold(
                extendBodyBehindAppBar: true, // แสดงพื้นที่ appbar แยก ให้ขายเต็มจอ
                appBar: AppBar(
                    leading: IconButton(
                      onPressed: (){
                        Navigator.of(context).pop();
                      },
                      icon: Icon(Icons.close,color: Colors.black),
                    ),
                    backgroundColor: Colors.transparent,
                    elevation: 0.0,
                ),
                body: Container(
                    child: SingleChildScrollView(
                      child: Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Column(
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: <Widget>[
                              Image.network(product.image),
                              SizedBox(height: 10.0),
                              Text(product.title,
                                style: Theme.of(context).textTheme.headlineMedium,
                              ),
                              SizedBox(height: 5.0),
                              Text('Price: \$ ${product.price}'),
                              SizedBox(height: 10.0),
                              Text('Price: ${product.description}'),
                            ],
                        ),
                      ),
                    )
                ),
              ),
            ),
        );
}
 
    การที่เราจะเปิดหน้ารายละเอียดขึ้นมา สิ่งที่คำสั่ง Navigator.of(context).push() ต้องการก็คือ route
ดังนั้นฟังก์ชั่น  _viewProduct จึงต้องคืนค่าเป็น Route<Object?> โดยตัวที่จะใช้สร้าง Route<Object>
ก็คือ DialogRoute() นั้นเอง ตัว DialogRoute จะมีคำสั่ง builder เพื่อสร้าง widget ตามที่เรากำหนดส่ง
ออกไปแสดง
 
1
2
3
4
5
6
7
8
9
// สร้างฟังก์ชั่น ที่คืนค่าเป็น route ของ object ฟังก์ชั่นนี้ มี context และ product เป็น parameter
static Route<Object?> _viewProduct(BuildContext context, Product product) {
  return DialogRoute<void>(
          context: context,
          builder: (BuildContext context) => // ใช้ arrow aฟังก์ชั่น
-----
------
  );
}
 
    ส่วนของการใช้งาน Dismissible widget ที่ครอบ Scaffold อธิบายไว้ในโค้ด เรากำหนดให้ตัว Scaffold
ไม่ต้องแสดงแถบ appBar แยกกับส่วนของ body แต่ให้รวมเป็นพื้นที่เดียวกัน เพื่อให้แสดงข้อมูลแบบเต็ม
แต่เรายังคงให้มีปุ่มปิดแทรกไว้ตรงมุมด้านซ้ายของ appBar  ส่วนของ body ก็เหมือนเดิมกับรูปแบบที่แสดง
ในไฟล์ product.dart
 
    เมื่อสร้างส่วนของฟังก์ชั่นใช้งาน DialogRoute แล้วก็มาทำต่อส่วนของการเรียกใช้งานใน ListTile
 
1
2
3
4
5
6
7
8
9
ListTile(
  leading: Image.network(product.image,width: 100.0,),
  title: Text(product.title),
  subtitle: Text('Price: \$ ${product.price}'),
  trailing: Icon(Icons.more_vert),
  onTap: (){
    Navigator.of(context).push(_viewProduct(context, product));                                          
  },
),
 
    ผลลัพธ์ที่ได้
 
 


 
 
    จะเห็นว่ากรณี Dialog เวลาเราปัดลงเพื่อปิดจะมองเห็นด้านหลังของหน้าที่เปิด dialog route ขึ้นมา
หรือจะปิดโดยกดที่ปุ่มก็ได้ 
 

    ไฟล์ home.dart แบบเต็ม กรณีใช้งาน DialogRoute

 
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
import 'dart:async';
import 'dart:convert';
   
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
 
import 'product.dart';
import '../models/product_model.dart';
     
class Home extends StatefulWidget {
    static const routeName = '/';
    
    const Home({Key? key}) : super(key: key);
     
    @override
    State<StatefulWidget> createState() {
        return _HomeState();
    }
}
     
class _HomeState extends State<Home> {
   
    // กำนหดตัวแปรข้อมูล products
    late Future<List<Product>> products;
    // ตัว ScrollController สำหรับจัดการการ scroll ใน ListView
    final ScrollController _scrollController = ScrollController();
     
    @override
    void initState() {
      super.initState();
      products = fetchProduct();
    }    
   
    Future<void> _refresh() async {
      setState(() {
       products = fetchProduct();
      });
    }
  
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('Home'),
            ),
            body: Center(
              child: FutureBuilder<List<Product>>( // ชนิดของข้อมูล
                future: products, // ข้อมูล Future
                builder: (context, snapshot) {
                  // มีข้อมูล และต้องเป็น done ถึงจะแสดงข้อมูล ถ้าไม่ใช่ ก็แสดงตัว loading
                  if (snapshot.hasData) {
                      bool _visible = false; // กำหนดสถานะการแสดง หรือมองเห็น เป็นไม่แสดง
                      if(snapshot.connectionState == ConnectionState.waiting){ // เมื่อกำลังรอข้อมูล
                        _visible = true; // เปลี่ยนสถานะเป็นแสดง
                      }
                      if(_scrollController.hasClients){ //เช็คว่ามีตัว widget ที่ scroll ได้หรือไม่ ถ้ามี
                        // เลื่อน scroll มาด้านบนสุด
                      _scrollController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
                      }
                      return Column(
                          children: [
                            Visibility(
                              child: const LinearProgressIndicator(),
                              visible: _visible,
                            ),
                            Container( // สร้างส่วน header ของลิสรายการ
                              padding: const EdgeInsets.all(5.0),
                              decoration: BoxDecoration(
                                color: Colors.orange.withAlpha(100),
                              ),                 
                              child: Row(                 
                                children: [
                                  Text('Total ${snapshot.data!.length} items'), // แสดงจำนวนรายการ
                                ],
                              ),
                            ),
                            Expanded( // ส่วนของลิสรายการ
                                child: snapshot.data!.isNotEmpty // กำหนดเงื่อนไขตรงนี้
                                ? RefreshIndicator(
                                  onRefresh: _refresh,
                                  child: ListView.separated( // กรณีมีรายการ แสดงปกติ
                                        controller: _scrollController, // กำนหนด controller ที่จะใช้งานร่วม
                                        itemCount: snapshot.data!.length,
                                        itemBuilder: (context, index) {
                                          Product product = snapshot.data![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: (){
                                                    Navigator.of(context).push(_viewProduct(context, product));                                          
                                                  },
                                                ),
                                              ],
                                            )
                                          );
                                          return card;
                                        },
                                        separatorBuilder: (BuildContext context, int index) => const SizedBox(),                   
                                  ),
                                )
                                : const Center(child: Text('No items')), // กรณีไม่มีรายการ
                            ),
                          ],
                        );
                  } else if (snapshot.hasError) { // กรณี error
                    return Text('${snapshot.error}');
                  }
                  // กรณีสถานะเป็น waiting ยังไม่มีข้อมูล แสดงตัว loading
                 return const RefreshProgressIndicator();
                },
              ), 
            ),         
        );
    }
 
    // สร้างฟังก์ชั่น ที่คืนค่าเป็น route ของ object ฟังก์ชั่นนี้ มี context และ product เป็น parameter
    static Route<Object?> _viewProduct(BuildContext context, Product product) {
      return DialogRoute<void>(
              context: context,
              builder: (BuildContext context) => // ใช้ arrow aฟังก์ชั่น
                Dismissible( // คืนค่าเป็น dismissible widget
                  direction: DismissDirection.vertical, // เมื่อปัดลงในแนวตั้ง
                  key: const Key('key'), // ต้องกำหนด key ใช้ค่าตามนี้ได้เลย
                  onDismissed: (_) => Navigator.of(context).pop(), // ปัดลงเพื่อปิด            
                  child: Scaffold(
                    extendBodyBehindAppBar: true, // แสดงพื้นที่ appbar แยก ให้ขายเต็มจอ
                    appBar: AppBar(
                        leading: IconButton(
                          onPressed: (){
                            Navigator.of(context).pop();
                          },
                          icon: Icon(Icons.close,color: Colors.black),
                        ),
                        backgroundColor: Colors.transparent,
                        elevation: 0.0,
                    ),
                    body: Container(
                        child: SingleChildScrollView(
                          child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Column(
                                mainAxisAlignment: MainAxisAlignment.start,
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  Image.network(product.image),
                                  SizedBox(height: 10.0),
                                  Text(product.title,
                                    style: Theme.of(context).textTheme.headlineMedium,
                                  ),
                                  SizedBox(height: 5.0),
                                  Text('Price: \$ ${product.price}'),
                                  SizedBox(height: 10.0),
                                  Text('Price: ${product.description}'),
                                ],
                            ),
                          ),
                        )
                    ),
                  ),
                ),
            );
    }       
   
}
  
// สรัางฟังก์ชั่นดึงข้อมูล คืนค่ากลับมาเป็นข้อมูล 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();
}
 
 
    หวังว่าเนื้อหานี้จะเพิ่มเติมความเข้าใจการใช้งาน widget ต่างๆ รวมถึงเรียนรู้รุปแบบการกำหนด
โครงสร้างของโปรเจ็ค flutter เพิ่มเติมเกี่ยวกับการจัดการ Data Model เนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม


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


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

ตัวอย่างการใช้งาน  เฉพาะส่วนดึงรายการสินค้ามาแสดง ยังไม่มีหน้ารายละเอียด

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


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


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

ตัวอย่างการใช้งาน  ดึงรายการสินค้ามาแสดง มีหน้าแสดงรายละเอียดสินค้าด้วย PageRoute

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


   เพิ่มเติมเนื้อหา ครั้งที่ 3 วันที่ 27-07-2024


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

ตัวอย่างการใช้งาน  ดึงรายการสินค้ามาแสดง มีหน้าแสดงรายละเอียดสินค้าด้วย DialogRoute

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


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



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



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









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









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











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