การใช้งาน Provider จัดการข้อมูล App State ใน Flutter

บทความใหม่ เดือนนี้ โดย Ninenik Narkdee
consumer app state flutter provider changenotifier

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



เนื้อหาตอนต่อไปนี้เราจะมาดูเกี่ยวกับการใช้งาน provider
จ้ดการข้อมูล app state ดังนั้นสิ่งที่เราต้องรู้ก่อนคือ provider
คืออะไร และ app state คืออะไร จากนั้นค่อยลงรายละเอียด
เกี่ยวกับโค้ดและตัวอย่างการใช้งาน
 
    *เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/961
 
 

ข้อมูล App State

    เป็นข้อมูลที่เราต้องการ หรือมีการใช้งานตามส่วนต่างๆ ของ app ยกตัวอย่างเช่น กรณี
มีการล็อกอินเข้าใช้งานใน app  เราต้องการให้สามารถใช้งานชื่อผู้ใช้ แสดงในหน้าต่างๆ ได้
คล้ายๆ กับ session หรือ cookie ใน web app  ข้อมูลเหล่านี้จะเรียกว่า app state หรือบางที
ก็อาจจะเรียกว่า shared state
    ถ้าเคยอ่านเกี่ยวกับ InheritedWidget ก็จะพอเข้าใจลักษณะการทำงาน ที่เราสามารถใช้ข้อมูล
ของ InheritedWidget ในหน้าต่างๆ ได้ ดูเพิ่มเติมที่บทความ http://niik.in/959 แต่วิธีนั้นก็เป็นวิธี
พื้นฐาน ในเนื้อหานี้เราจะใช้งาน provider package จัดการกับข้อมูลแทน
    หลายๆ บทความที่ผ่านมา เราได้สร้างหน้า app และใช้งานข้อมูล local state หรือข้อมูลชั่วคราว
ที่แสดงเมื่ออยู่ในหน้า app นั้นๆ หรือแม้แต่ตัวอย่าง demo ของ flutter ที่เป็นการเพิ่มจำนวนการนับ
เมื่อกดเพิ่มจำนวนที่ตัว FloatingActionButton สมมติเช่น เราอยู่หน้า A มีปุ่มบวกเพิ่มตัวเลข และมีข้อ
ความแสดงจำนวนที่เพิ่มขึ้น 
 
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
..........
}
 
    ตัวแปร _counter คือข้อมูล local state ที่มีการกำหนดและใช้งนหน้า widget นั้นๆ  แต่ถ้าเราอยากให้ 
_counter สามารถใช้งานในหน้าอื่นๆ ได้ หรือก็คือเราต้องการทำให้เป็น app state อยากให้ไปแสดงจำนวนที่
นับเพิ่มในส่วนอื่น หน้าอื่นที่อยู่ด้านนอกของ widget นี้ เราจะสามารถทำได้อย่างไร นี่คือแนวทางที่เราจะเรียนรู้
วิธีการ รวมถึงการใช้งาน provider เข้ามาช่วย
    อย่างไรก็ตาม ก่อนที่เราจะใช่งานข้อมูลที่เป็น app state เราต้องพิจารณาถึงข้อมูลนั้นๆ ก่อนว่า ข้อมูลนั้นๆ
ใช้กับ widget เดียว  หรือ 2 - 3 widget หรือ ใช้เกือบทุกส่วนใน app  ถ้าใช้แค่ใน widget เดียว เราไม่จำเป็น
ต้องกำหนดเป็น app state ใช้เป็นแบบ local state ก็พอ แต่ถ้า อาจจะมีใช้งานข้อมูลนั้นๆ ในหลายหน้า หลาย
widget ข้อมูลนั้นก็ควรจะใช้เป็น app state 


 
 

รู้จักกับ Provider

    provider เป็นกลไกหรือวิธีการในการจัดการกับข้อมูลหรือคำสั่งการทำงานใน widget เพื่อส่งต่อข้อมูลหรือ
คำสั่งการทำงานนั้นๆ ไปยังส่วนต่างๆ เข้าใจอย่างง่ายก็คือ ส่งต่อข้อมูลและการทำงานไปยัง widget ลูกที่อยู่
ด้านในทั้งหมดเป็นทอดๆ ในลักษณะเช่นเดียวกับ InheritedWidget
    ก่อนใช้งาน provider เราจำเป็นต้องทำการติดตั้ง package นี้ก่อนโดยทำการเพิ่มไปในไฟล์ pubspec.yaml
ประมาณนี้
 
dependencies:
  flutter:
    sdk: flutter

  provider: ^6.0.1
 
    จากนั้นในหน้าที่มีการใช้งาน provider เราก็ import package มาใช้งานในลักษณะดังนี้
 
import 'package:provider/provider.dart';
 
    สำหรับการใช้งาน provider สิ่งที่เราจำเป็นต้องรู้เพิ่มเติมเบื้องต้นในการใช้งาน ก็คือ
 
    - ChangeNotifier
    - ChangeNotifierProvider
 
    ทั้งสองส่วนนี้ จะอธิบายไปพร้อมๆ กับตัวอย่างและโค้ดทดสอบ
 

 

ใช้งาน ChangeNotifier

    ChangeNotifier เป็น class ที่ทำหน้าที่ แจ้งเตือนเมื่อมีการเปลี่ยนแปลงข้อมูล app state ดังนั้น
ข้อมูล app state ที่เราจะใช้ ต้องใช้งานร่วมกับ class นี้
    โค้ดตัวอย่างเที่เราจะใช้งานคือ เราจะสร้างข้อมูลที่เป็น app state เป็นการนับจำนวนการกดเพิ่มค่า
ข้อมูล เพื่อให้จำนวนค่าที่เพิ่มน้้น สามารถนำไปแสดงในหลายๆ หน้าได้
    ให้เราสร้างไฟล์ข้อมูลสำหรับใช้เป็น app state ชื่อว่า counter_model.dart ไว้ในโฟลเดอร์
    lib > models > counter_model.dart
 
    ไฟล์ counter_model.dart
 
import 'package:flutter/material.dart';

// สร้างข้อมูล app state ชื่อ Counter ใช้งานร่วมกับ ChangeNotifier
class Counter with ChangeNotifier {
  int _count = 0; // กำหนดชนิดข้อมูล และค่าเริ่มต้น

  // กำหนด getter คือค่าจากตัวแปร _count
  int get count => _count;

  // กำหนดคำสั่ง และการทำงาน
  void increment() {
    _count++; // เพิ่มจำนวน
    notifyListeners(); // แจ้งเตือนการเปลี่ยนแปลงข้อมูล
  }

  // กำหนดคำสั่ง และการทำงาน
  void reset() {
    _count = 0; // รีเซ็ตค่า
    notifyListeners(); // แจ้งเตือนการเปลี่ยนแปลงข้อมูล
  }  

}
 
    ใน class ข้อมูล app state นี้มีแค่การกำหนดตัวแปรข้อมุล คำสั่งการเพิ่มจำนวน การรีเซ็ต 
และการกำหนด getter หรือฟังก์ชั่นการอ่านค่าข้อมูลผ่าน property  สังเกตว่าในคำสั่งการเพิ่ม
จำนวน และการรีเซ็ตค่า จะมีการใช้งานฟังก์ชั่น notifyListeners() เพื่อเป็นการแจ้งเตือนของ
การเปลี่ยนแปลงข้อมูล ไปให้ provider ส่งต่อไปใช้งาน
 

 

การกำหนด Provider

    เมื่อเราจัดการข้อมูลส่วนของ app state แล้วต่อไปก็เป็นการใช้งาน provider เพื่อทำหน้าที่ส่งต่อ
ข้อมูลไปใช้งานในหน้าต่างๆ ของ app การกำหนด provider จะกำหนดไว้ส่วนบนสุดของ app หรือ
ส่วนบนสุดของส่วนที่จะใช้งานข้อมูล ในที่นี้เราจะกำหนดไว้ที่ root ของ app และจะใช้วิธีการกำหนด
การใช้งาน provider แบบที่ไม่รองรับการแจ้งเตือนการเปลี่ยนแปลงข้อมูล เป็นตัวอย่างก่อนดังนี้
 
    ไฟล์ main.dart
 
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import './screen/launcher.dart';

import 'models/counter_model.dart';


void main() {  runApp(const MyApp());}
 
// ส่วนของ Stateless widget
class MyApp extends StatelessWidget{
   const MyApp({Key? key}) : super(key: key);
   
    @override
    Widget build(BuildContext context) {
      return Provider(
        create: (context) => Counter() ,
        child: MaterialApp(
                  theme: ThemeData(
                      primarySwatch: Colors.pink,
                  ),
                  title: 'First Flutter App',
                  initialRoute: '/', // สามารถใช้ home แทนได้
                  routes: {
                      Launcher.routeName: (context) => Launcher(),
                  },
          )
        );
    }
}
 
   เนื่องจากตัวอย่างนี้เรากำหนดใช้งาน provider เดียวเป็นแนวทางเบื้องต้นประกอบคำอธิบาย
 
Provider(
    create: (context) => Counter() ,
    child:......

)
 
    เราส่งต่อข้อมูล app state ที่ชื่อ Counter() ผ่าน property ชื่อ create เข้าไปใช้งาน provider
ลักษณะนี้ไม่รองรับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลงข้อมูล นั่นหมายความว่า ถ้าเพิ่มจำนวนแล้ว
จำนวนเพิ่ม แต่ไม่มีการแสดง widget ใหม่ทันทีที่เพิ่ม  เพราะไม่มีการกำหนดการแจ้งเตือน
 
     เราจะใช้ไฟล์ about.dart และ profile.dart ประกอบการทำงาน โดย ไฟล์ about.dart จะแสดง
การดึงข้อมูลจำนวน มาแสดง และมีปุ่ม reset ค่าเป็น 0 ส่วนไฟล์ profile.dart จะมีการดึงข้อมูลมาแสดง
และมีปุ่มสำหรับ increment หรือบวกเพิ่มจำนวน
 
     ไฟล์ about.dart บางส่วน
 
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/counter_model.dart';
  
class About extends StatefulWidget {
    static const routeName = '/about';
 
    const About({Key? key}) : super(key: key);
  
    @override
    State<StatefulWidget> createState() {
        return _AboutState();
    }
}
  
class _AboutState extends State<About> {
  
    @override
    Widget build(BuildContext context) {
  
        return Scaffold(
            appBar: AppBar(
                title: Text('About Us'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('About Us Screen'),
                        Text(
                          '${context.watch<Counter>().count}',
                          key: const Key('counterState'),
                          style: Theme.of(context).textTheme.headline4),  
                        ElevatedButton(
                          onPressed: () => context.read<Counter>().reset(),
                          child: Text('Reset Counter'),
                        ),
                    ],
                )
            ),
        );
    }
}
 
     ไฟล์ profile.dart
 
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/counter_model.dart';
  
class Profile extends StatefulWidget {
    static const routeName = '/profile';
 
    const Profile({Key? key}) : super(key: key);
  
    @override
    State<StatefulWidget> createState() {
        return _ProfileState();
    }
}
  
class _ProfileState extends State<Profile> {
  
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('Profile'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('Profile Screen'),
                        Text('You have pushed the button this many times:'),
                        Count(),
                    ],
                )
            ),
            floatingActionButton: FloatingActionButton(
              key: const Key('increment_Button'),
              onPressed: () => context.read<Counter>().increment(),
              child: const Icon(Icons.add),
            ),
        );
    }
}


class Count extends StatelessWidget {
  const Count({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
        '${context.watch<Counter>().count}',
        key: const Key('counterState'),
        style: Theme.of(context).textTheme.headline4);
  }
}
 
    ผลลัพธ์การทำงาน



 
 
    เมื่อเราเปิด app ขึ้นมาแล้วไปหน้า about แล้วไปที่หน้า profile จะเห็นว่าหน้าทั้งสองทำการดึงข้อมูล
app state มาแสดง ซึ่งค่าเริ่มต้นเป็น 0 และพอเรากดเพิ่มจำนวนในหน้า profile 4 ครั้ง จะไม่เห็นการเปลี่ยน
แปลงใดๆ แต่พอเรากดกลับมาที่หน้า about ปรากฏว่าตัวเลขเพิ่มเป็น 4 ตามจำนวนที่กด ทั้งนี้เพราะเมื่อเรา
กลับมาหน้า about ก็จะมีการสร้าง widget หรือการ build เกิดขึ้น และก็มีการใช้งาน provider ดึงข้อมูล app 
state มาแสดง ต่อไปเรากลับไปหน้า profile ก็ปรากฏว่าตอนนี้เลขก็เปลี่ยนเป็น 4 เหมือนกัน ก็เพราะมีการ build
ใหม่เกิดขึ้นเนื่องจากเป็นการโหลดหน้า app นั้นๆ ใหม่ และ พอเรากลับไปหน้า about แล้วกด reset ค่า app state
ก็ไม่มีการเปลี่ยนแปลง แต่จริงๆ แล้วมีการทำคำสั่ง reset แล้ว เพียงแต่ว่าไม่มีการแจ้งเตือน  จากนั้นเรากกลับมา
หน้า profile ตัวเลขก็ถูก reset เป็น 0 
    แสดงว่าตอนนี้เราสามารถใช้ข้อมูล app state โดยการกำหนด provider ได้แล้ว แต่ การกำหนด provider รูปแบบ
นี้ใช้สำหรับข้อมูลที่ไม่มีการเปลี่ยนแปลง ก่อนไปต่อ มาดูการทำงานของการแสดงข้อมูลจาก app state ใน 2
รูปแบบคำสั่งคือ
 
context.watch() // ใช้สำหรับทำคำสั่งและรองรับการ build ใหม่ เมื่อมีการแจ้งเตือน
context.read() // ใช้สำหรับทำคำสั่งแต่ะจะไม่ build ใหม่ 
 
    ตัวอย่าง
 
context.watch<Counter>().count  // เรียก getter ข้อมูลจากตัวแปร _count
context.read<Counter>().increment() // เรียกทำคำสั่งเพิ่มจำนวน
context.read<Counter>().reset() // เรียกทำคำสั่งรีเซ็ตจำนวน
 
    แต่ตอนนี้เรากำหนด provider ในแบบยังไม่รองรับการแจ้งเตือนการเปลี่ยนข้อมูล จึงทำงานในคำสั่งได้
เท่านั้นไม่มีการ build ใหม่ในทันที
 
    ในไฟล์ profile.dart เราสร้าง Count class เพื่อใช้ในกรณีมีการ build ใหม่
 
class Count extends StatelessWidget {
  const Count({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
        '${context.watch<Counter>().count}',
        key: const Key('counterState'),
        style: Theme.of(context).textTheme.headline4);
  }
}
 
    นั่นหมายความว่า ถ้ามีการ build จะไม่ build ทั้งหมด แต่จะมา build เฉพาะในส่วนของ widget ที่มีการ
เปลี่ยนแปลงข้อมูลจากการใช้งาน provider 
 
 
 

การใช้งาน ChangeNotifierProvider

    เป็น widget ที่รองรับการใช้งาน การแจ้งเตือนเมื่อมีการเปลี่ยนแปลง หรือก็คือ ChangeNotifier class
ที่เรากำหนด ถ้าเราใช้งานการกำหนด provider ด้วยวิธีนี้ เมื่อมีการเปลี่ยนแปลงข้อมูล ก็จะมีการ build ใหม่
และแสดงข้อมุล app state ที่มีการเปลี่ยนแปลงในทันที
    ดังนั้นเราจะเปลี่ยนการเรียกใช้งาน provider เพราะ ส่วนใหญ่แล้ว การใช้งาน provider ควรรองรับทั้ง
แบบกำหนดข้อมูลที่เปลี่ยนแปลง และไม่เปลี่ยนแปลงได้ เราจะใช้เป็น MultiProvider และใช้งาน
ChangeNotifierProvider ด้านในอีกที เป็นดังนี้
 
    ไฟล์ main.dart บางส่วน
return MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (context) => Counter()),
  ], 
  child: MaterialApp(
            theme: ThemeData(
                primarySwatch: Colors.blue,
            ),
            title: 'First Flutter App',
            initialRoute: '/', // สามารถใช้ home แทนได้
            routes: {
                Launcher.routeName: (context) => Launcher(),
            },
    )
  );
 
    สังเกตว่า ตอนนี้ เราสามารถกำหนด provider ได้หลายค่า เพราะรองรับการกำหนดแบบ List
แต่ในตัวอย่างเรากำหนดตัวเดียวคือ ใช้เป็น ChangeNotifierProvider ที่รองรับการ buld เมื่อมีการ
แจ้งเตือนการเปลี่ยนแปลงข้อมูล
 
    ผลลัพธ์ที่ได้


 
 
 
    ข้อมูลเปลี่ยนแปลงทันทีเมื่อมีการกดเพิ่มจำนวน และเมื่อกลับมาหน้า about แล้วกด reset ค่าก็
แสดงเป็น 0 ทันที เพราะมีการ build ใหม่เมื่อมีการแจ้งการเปลี่ยนแปลงข้อมุล app state
    อย่างไรก็ตาม สังเกตในไฟล์ about เรากำหนดการใช้งาน 
 
context.watch<Counter>().count}
 
    โดยไม่ได้แยกเป็น widget ออกมาเหมือนหน้า profile เวลา build ใหม่ ก็จะเป็นการ build ตั้งแต่
Scaffold ของหน้านั้น ที่มีการ return ค่า หากเป็นไปได้ เพื่อประสิทธิภาพการทำงาน เราควรสร้างเป็น
widget เหมือนหน้า profile เพื่อใช้งาน ในลักษณะดังนี้
 
class _AboutState extends State<About> {
  
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('About Us'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('About Us Screen'),
                        Count(),  
                        ElevatedButton(
                          onPressed: () => context.read<Counter>().reset(),
                          child: Text('Reset Counter'),
                        ),
                    ],
                )
            ),
        );
    }
}

class Count extends StatelessWidget {
  const Count({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
        '${context.watch<Counter>().count}',
        key: const Key('counterState'),
        style: Theme.of(context).textTheme.headline4);
  }
}
 
 
 

การใช้งาน Consumer

    ถ้าเราอยากใช้งานข้อมูล app state ที่ส่งมาโดย provider และรองรับการ rebuild เฉพาะส่วนเหมือนการ
สร้าง widget แยก แบบตัวอย่างด้านบน ก็สามารถทำได้โดยใช้งาน Consumer widget แทนได้ โดยตัว
Consumer จะทำหน้าที่รับค่า app state แล้วส่งต่อไปยังคำสั่ง builder เพื่อใช้งาน เรามาลองใช้งานกับไฟล์
about แทนการแยกเป็น class Count ก็จะได้เป็นดังนี้
 
class _AboutState extends State<About> {
  
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('About Us'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('About Us Screen'),
                        Consumer<Counter>(
                          builder: (context, counter, child) {
                              return Text(
                                  '${counter.count}',
                                  key: const Key('counterState'),
                                  style: Theme.of(context).textTheme.headline4);
                            },
                        ),  
                        ElevatedButton(
                          onPressed: () => context.read<Counter>().reset(),
                          child: Text('Reset Counter'),
                        ),
                    ],
                )
            ),
        );
    }
}
 

    รูปแบบการทำงานของ Consumer

 
Consumer<T>(
  builder: (context, ตัวแปรอ้างอิง T, child) {
      return .....
    },
 //  child: // มี child หรือไม่ก็ได้ ส่วนนี้จะไม่มีการ rebuild เพราะอยู่นอก builder
),  
 
    สังเกตว่าเราสามารถเรียกใช้งาน Counter instance ผ่านตัวแปร counter
 
Consumer<Counter>(
  builder: (context, counter, child) {
      return Text(
          '${counter.count}',
          key: const Key('counterState'),
          style: Theme.of(context).textTheme.headline4);
    },
),  
 
    เมื่อข้อมูล app state มีการเปลี่ยนแปลง เฉพาะส่วนของ Consumer เท่านั้นที่มีการ rebuild ใหม่
 
    เราสามารถกำหนดตัวแปร เพื่อเรียกใช้งาน การทำคำสั่งต่างๆ ของ app state เพื่อให้สะดวกเวลาใช้ได้
 
Counter counter = context.read<Counter>();
 
    ตัวอย่างการใช้งาน ปุ่ม reset
 
class _AboutState extends State<About> {
  
    @override
    Widget build(BuildContext context) {
        Counter counter = context.read<Counter>();
        // หรือ กรรณีไม่ได้ใช้ค่าจาก Cusumer 
        // Counter counter = context.watch<Counter>();
        
        return Scaffold(
            appBar: AppBar(
                title: Text('About Us'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('About Us Screen'),
                        Consumer<Counter>(
                          builder: (context, counter, child) {
                              return Text(
                                  '${counter.count}',
                                  key: const Key('counterState'),
                                  style: Theme.of(context).textTheme.headline4);
                            },
                        ),  
                        ElevatedButton(
                          onPressed: () => counter.reset(),
                          child: Text('Reset Counter'),
                        ),
                    ],
                )
            ),
        );
    }
}
 
    กรณีเราอยากส่งปุ่ม รีเซ็ตเข้าไปเป็น child ของ Consumer สามารถทำได้ดังนี้
 
Consumer<Counter>(
  builder: (context, counter, child) {
      return Column(
        children: [
          Text(
            '${counter.count}',
            key: const Key('counterState'),
            style: Theme.of(context).textTheme.headline4),
          if (child != null) child,
        ],
      );
    },
  child: ElevatedButton(
    onPressed: () => counter.reset(),
    child: Text('Reset Counter'),
  ),
),  
 
    สังเกตว่า Consumer เรากำหนด child เข้าไป ตัว child นี้จะไม่แสดงจนกว่าเราจะเรียกใช้งานใน 
คำสั่ง builder ซึ่งเป็น argument ตัวที่ 3 ที่ถูกส่งเข้าไป 
 
if (child != null) child, // ตรวจสอบว่ามีการกำหนด child หรือไม่ 
 
    ถ้ามีก็ให้แสดง ซึ่งในตัวอย่างก็เป็นการแสดงต่อจากบรรทัดข้อความ ที่อยู่ใน Column widget อีกที
 
 
    หวังว่าเนื้อหาเกี่ยวกับการใช้ข้อมูล App State ผ่านการเรียกใช้งาน Provider นี้ จะเป็นแนวทาง
ในการปรับประยุกต์ใช้งานต่อไป เช่น การประยุกต์กำหนดการใช้งาน theme การกำหนดสถานะการล็อกอิน
หรือข้อมูลการล็อกอิน การใช้งานข้อมูลระหว่างหน้าอื่นๆ เป็นต้น สำหรับเนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม


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



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









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









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











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