ทำความรู้จักและใช้งาน Riverpod ใน Flutter เบื้องต้น ตอนที่ 1

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

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

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


จากตอนที่แล้ว เราได้เห็นความสำคัญของการจัดการข้อมูล state
โดยเฉพาะข้อมูลที่มีจำนวนมากและมีความซับซ้อน การเพิ่มความเร็ว
ด้วยวิธีการ cache เพื่อให้ข้อมูลถูกเรียกใช้งานได้เร็วขึ้นโดยวิธีการสร้าง
เป็นไฟล์แล้วเรียกใช้งาน ก็ถึอเป็นอีกวิธีจัดการกับข้อมูล state อย่างไรก็ดี
ยังมีวิธีที่จะเพิ่มความเร็วของการโหลดและแสดงข้อมูลในภาพรวมได้อีก นั่นคือ
การใช้งาน Riverpod ซึ่เป็น package สำหรับจัดการข้อมูล state โดยทำงาน
ในลักษณะเดียวกับ Provider ซึ่งเราเคยแนะนำไว้แล้ว ที่บทความตามลิ้งค์ด้านล่าง
 
การใช้งาน Provider จัดการข้อมูล App State ใน Flutter http://niik.in/1046
 
การใช้งาน Riverpod จะทำให้เราสามารถแสดงข้อมูลได้เร็วขึ้น อย่างตัวอย่างตอนที่แล้ว เมื่อเรา
ทำการโหลดข้อมูลจากไฟล์ แน่นอนว่าจะเร็วกว่าการโหลดจาก server แต่ถ้าเราเปิดไปหน้าอื่น
แล้วกลับมาหน้าเดิม เราจะพบว่าข้อมูลก็ยังมีจังหวะการโหลดข้อมูลจากไฟล์ให้เราได้เห็น โดย
สังเกตจากจังหวะที่มีตัว loading หมุนสักพัก ถึงแม้จะไม่นานมาก หรือบางครั้งอาจจะไม่เห็นเลย
แต่การใช้ riverpod เข้ามาช่วย จะทำให้เราเห็นความแตกต่างคือ ข้อมูลจะแสดงแทบจะทันทีที่
เรากลับมาหน้าเดิม เป็นไปในลักษณะที่ข้อมูลพร้อมใช้มากขึ้นกว่าเดิม เพราะมีการทำการ cache 
เพิ่มเติมเข้ามาอีกโดยเก็บไว้ในหน่วยความจำ ทำให้บางครั้งเวลาเราสลับไปหน้าอื่นๆ อาจจะแทบไม่
เห็นการโหลด นอกจากนั้น riverpod ยังสามารถประยุกต์ใช้งานกับ state อื่นๆ ได้อีกมากมาย
อย่างไรก็ดี ด้วยความสามารถที่เพิ่มขึ้น เราก็จำเป็นจะต้องเข้าใจและจัดการการทำงานให้เหมาะสม
ไปด้วย ไม่เช่นนั้นก็อาจจะมีผลกับการใช้งานหน่วยความจำและส่งผลต่อประสิทธิภาพโดยรวมของ
แอปของเราได้
 
 

ติตดั้ง flutter_riverpod

    ให้ทำการเพิ่มส่วนของ flutter_riverpod package เข้าไปสำหรับใช้งาน 
 
flutter_riverpod: ^2.5.1
 
    จากนั้นทำการเพิ่ม ProviderScope เข้าไปในส่วน main() ฟังก์ชั่น ดังรูป เพื่อให้แอปของเรา
พร้อมใช้งาน riverpod โดยเราจะใช้กับทั้งหมดของ แอป
 
void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}
 
    ก่อนจะลงไปต่อที่ตัวอย่างและรายละเอียดเรามาทำความรู้จักกับ Riverpod กันก่อนดังนี้
 
 

Riverpod คืออะไร

Riverpod เป็น state management library สำหรับ Flutter ที่ถูกพัฒนาโดยผู้สร้างเดียวกัน
กับ Provider โดยมีการออกแบบให้มีประสิทธิภาพและยืดหยุ่นมากขึ้น การใช้ Riverpod ช่วยให้การ
จัดการสถานะ (state) ในแอปพลิเคชัน Flutter ง่ายขึ้นและมีความปลอดภัยมากขึ้น
 

หลักการใช้งาน Riverpod

    - State Management: Riverpod ถูกใช้ในการจัดการสถานะในแอปพลิเคชัน Flutter 
เช่น การเก็บและอัปเดตข้อมูลที่ใช้งานร่วมกันในหลายๆ ส่วนของแอปพลิเคชัน
    - Dependency Injection: Riverpod ช่วยจัดการการแทรกการพึ่งพา (dependency 
injection) โดยง่าย โดยสามารถประกาศและใช้ providers เพื่อจัดการการพึ่งพาระหว่า Object
ต่างๆ ได้อย่างมีประสิทธิภาพ
    - Auto Dispose: Riverpod มีความสามารถในการจัดการกับ providers ที่ไม่จำเป็นต้องใช้
งานอีกต่อไปโดยอัตโนมัติ (auto dispose) ซึ่งช่วยประหยัดหน่วยความจำ
    - Type Safety: Riverpod รองรับการตรวจสอบประเภท (type safety) ซึ่งช่วยลดข้อ
ผิดพลาดในระหว่างการพัฒนา
 

ตัวอย่างการใช้งาน

    - Provider ใช้สำหรับการจัดการค่าคงที่ (immutable values)
    มักใช้กำหนด: ค่าคงที่หรือค่าที่ไม่เปลี่ยนแปลงบ่อย ๆ และไม่ต้องการการอัปเดตซ้ำบ่อยๆ
 
// ตัวอย่างการกำหนด
final greetingProvider = Provider<String>((ref) {
  return 'Hello, Riverpod!';
});
 
    - StateProvider ใช้สำหรับการจัดการ state แบบง่ายๆ ซึ่งสามารถเก็บค่าหรือข้อมูลใดๆ   
ที่สามารถเปลี่ยนแปลงได้ในระหว่างการทำงานของแอปพลิเคชัน
    มักใช้ในการกำหนดเงื่อนไข filter หรือใช้กับ state object ค่าง่ายๆ อย่างค่า int 
bool, String หรือ List ที่สามารถเปลี่ยนแปลงได้
 
// ตัวอย่างการกำหนด
final counterProvider = StateProvider<int>((ref) => 0);
 
    - NotifierProvider ใช้สำหรับการจัดการสถานะโดยใช้คลาสที่สืบทอดจาก Notifier 
ซึ่งเป็นคลาสที่สามารถกำหนดสถานะที่ต้องการและมีการจัดการสถานะเองภายในได้
    มักใช้กำหนด: ค่าที่สามารถเปลี่ยนแปลงได้ โดยใช้ Notifier ในการจัดการสถานะที่อาจจะ
มีความซับซ้อนขึ้นเล็กน้อย
 
// ตัวอย่างการกำหนด
class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;

  void increment() => state++;
}

final counterProvider = NotifierProvider<CounterNotifier, int>(() => 
CounterNotifier());
 
 
    - *StateNotifierProvider ใช้สำหรับการจัดการสถานะที่ซับซ้อนขึ้น มีการอัปเดตสถานะ
ภายในอย่างมีแบบแผน โดยใช้ใช้คลาสที่สืบทอดจาก StateNotifier 
    มักใช้กำหนด: ค่าที่เปลี่ยนแปลงได้ โดยใช้ StateNotifier ซึ่งเหมาะสำหรับสถานะที่ซับซ้อนและ
ต้องการการควบคุมการเปลี่ยนแปลงที่ชัดเจน
 
// ตัวอย่างการกำหนด
class CounterStateNotifier extends StateNotifier<int> {
  CounterStateNotifier() : super(0);

  void increment() => state++;
}

final counterProvider = StateNotifierProvider<CounterStateNotifier, int>(() =>
CounterStateNotifier());
*ปัจจุบันแนะนำให้ใช้เป็นแบบ NotifierProvider แทน
 
 
    - FutureProvider ใช้สำหรับการจัดการค่าที่ได้จาก Future
    มักใช้กำหนด: ค่าที่ได้จาก Future ซึ่งต้องรอการประมวลผล เช่น การดึงข้อมูลจาก API
 
// ตัวอย่างการกำหนด
final userNameProvider = FutureProvider<String>((ref) async {
  // จำลองการดึงข้อมูลจาก API
  await Future.delayed(Duration(seconds: 2));
  return 'John Doe';
});
 
    - StreamProvider ใช้สำหรับการจัดการค่าที่ได้จาก Stream
    มักใช้กำหนด: ค่าที่ได้จาก Stream ซึ่งมีการอัปเดตข้อมูลอย่างต่อเนื่อง เช่น การรับข้อมูล
แบบเรียลไทม์
 
// ตัวอย่างการกำหนด
final counterStreamProvider = StreamProvider<int>((ref) async* {
  // จำลองการนับเลข
  int counter = 0;
  while (true) {
    await Future.delayed(Duration(seconds: 1));
    yield counter++;
  }
});
 

    รูปแบบการกำหนด Provider ใน Riverpod จะอยู่ในลักษณะดังนี้

 
final name = SomeProvider.someModifier<Result>((ref) {
  <your logic here>
});
 
    name ก็คือ ชื่อตัวแปร provider ที่เราต้องการกำหนดเพื่อเรียกใช้งาน โดยจะเป็น
final และเป็นตัวแปร global (top-level) มักใช้รูปแบบชื่อเป็น lowerCamelCase
    SomeProvider เป็นรูปแบบ provider ที่เราจะใช้งาน ซึ่งจะใช้งานแบบไหนขึ้นกับ
ผลลัพธ์ของข้อมูลที่เราต้องการใช้งานเป็นสำคัญ  มักใช้รูปแบบชื่อเป็น PascalCase
    someModifier เป็นส่วนปรับแต่งของ provider เพิ่มเติม จะกำหนดหรือไม่ก็ได้ ใน Riverpod
ตอนนี้มี 2 รายรูปแบบให้เลือกใช้ คือ 
        - autoDispose กำหนดให้กับ Provider ใน Riverpod เพื่อให้ระบบทำการล้างข้อมูลแคช
(cache) โดยอัตโนมัติเมื่อ Provider นั้นหยุดถูกใช้งาน เมื่อกำหนด autoDispose ให้กับ 
Provider, หากไม่มี Widget ใด ๆ ใช้ Provider นั้นอีกต่อไป ข้อมูลที่เก็บไว้ในแคชของ Provider
จะถูกล้างออกโดยอัตโนมัติ ซึ่งช่วยลดการใช้หน่วยความจำที่ไม่จำเป็นและทำให้แอปพลิเคชันทำงานได้
อย่างมีประสิทธิภาพมากขึ้น
        - family กำหนดให้สามารถส่งอาร์กิวเมนต์ไปยัง Provider เพื่อใช้ในการสร้างหรือประมวล
ผลข้อมูลได้ โดยปกติแล้ว Provider จะทำงานโดยไม่ต้องพึ่งพาข้อมูลภายนอก แต่เมื่อใช้ family เรา
สามารถส่งค่าเข้าไปเพื่อให้ Provider ทำงานตามค่าที่ได้รับมา ซึ่งเป็นประโยชน์ในการใช้งานที่ต้องการ
ค่าพารามิเตอร์ที่ต่างกันสำหรับแต่ละการเรียกใช้
    Ref เป็นออบเจ็กต์ที่ใช้สำหรับการโต้ตอบกับ Provider อื่น ๆ ภายในแอปพลิเคชัน
    Result  เป็นส่วนกำหนดชนิดข้อมูล (data type) ที่ฟังก์ชัน Provider จะคืนค่าออกมา
    <your logic here> เป็นส่วนสำหรับกำหนดฟังก์ชันของ Provider (The provider 
function) เป็นที่ที่เราวางตรรกะและกระบวนการทำงานของ Provider ฟังก์ชันนี้จะถูกเรียกใช้เฉพาะ
ครั้งแรกที่มีการอ่าน Provider นั้น ๆ เท่านั้น  เมื่ออ่าน Provider ครั้งต่อไป ฟังก์ชันจะไม่ถูกเรียกใช้
ใหม่ แต่จะคืนค่าที่เก็บไว้ในแคชจากการเรียกใช้ครั้งก่อนแทน
 
    เราได้รู้จักเกี่ยวกับ riverpod เบื้องต้นคร่าวๆ ไปแล้ว มาลองใช้งานอย่างง่าย โดยสร้างรูปแบบ
การทำงานตัวอย่างคือ มีปุ่ม บวก เพิ่มจำนวนค่าตัวเลข ที่เรากำหนดเป็นค่า state คล้ายๆ กับ
demo เริ่มต้นของ flutter ที่เราคุ้นเคย 
 
เนื้อหานี้ใช้โค้ดตัวอย่างเริ่มต้น จากบทความ ตามลิ้งค์นี้ http://niik.in/961
โดยใช้ โค้ดตัวอย่างจากส่วน เพิ่มเติมเนื้อหา ครั้งที่ 2 

 

การใช้งาน Flutter Riverpod

    ให้เราสร้างไฟล์ Provider ในตัวอย่างนี้จะใช้เป็น 
    lib > providers > counter_provider.dart 
 

ไฟล์ counter_provider.dart

 
import 'package:flutter_riverpod/flutter_riverpod.dart';

final counterProvider = StateProvider<int>((ref) => 0);
 
    เป็นไฟล์อย่างง่ายมีแค่ 2 บรรทัด คือส่วนที่ import riverpod มาใช้งาน กับส่วนที่กำหนด
StateProvider โดยกำหนด ชื่อเป็น counterProvider ใช้รูปแบบ   StateProvider คืน
ค่าเป็นข้อมูล <int> โดยค่าเริ่มต้นที่คืนออกมา มีค่าเป็น 0 ในตัวอย่างใช้รูปแบบ arrow function
 
// รูปแบบเต็มแบบกำหนดปีกกาฟังก์ชั่น
final counterProvider = StateProvider<int>((ref) {
  return 0;
});
 
    ต่อไปก็เป็นส่วนของการนำไปใช้งาน สิ่งที่เราจะต้องปรับ เมื่อมีการใช้งานร่วมกับ riverpod ก็คือ
การใช้งาน widget  จากรูปแบบเดิม เราใช้เป็น   StatefulWidget 
 
// เฉพาะบางส่วน
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> {
......
....
 
ก็จะแก้ไขเป็น 
 
class Home extends ConsumerStatefulWidget {
  static const routeName = '/home';

  const Home({Key? key}) : super(key: key);

  @override
  ConsumerState<ConsumerStatefulWidget> createState() {
    return _HomeState();
  }
}

class _HomeState extends ConsumerState<Home> {
 
โดยเพิ่มคำว่า Consumer เพิ่มเข้าไป 
 
ถ้าเป็น StatelessWidget เราจะเปลี่ยนเป็นชื่อ ConsumerWidget แทน
 
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
 
เปลี่ยนเป็น
 
class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  // Notice how "build" now receives an extra parameter: "ref"
  Widget build(BuildContext context, WidgetRef ref) {

 
โดยในส่วนของฟังก์ชั่น build จะมี parameter "ref" เพิ่มเข้ามา
 
    ในที่นี้ในตัวอย่างเราเป็นแบบ StatefulWidget
 

ไฟล์ home.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/counter_provider.dart'; // Import the provider

class Home extends ConsumerStatefulWidget {
  static const routeName = '/home';

  const Home({Key? key}) : super(key: key);

  @override
  ConsumerState<ConsumerStatefulWidget> createState() {
    return _HomeState();
  }
}

class _HomeState extends ConsumerState<Home> {
  @override
  Widget build(BuildContext context) {
    final counter = ref.watch(counterProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        leading: IconButton(
          icon: Icon(Icons.menu),
          onPressed: () {
            Scaffold.of(context).openDrawer();
          },
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text('$counter')
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {     
          // Increment the counter value
          ref.read(counterProvider.notifier).state++;
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
 
ในโค้ดตัวอย่าง ทำการอ่านค่า state ใน Provider มาเก็บในตัวแปร  เพื่อนำไปใช้งาน
 
final counter = ref.watch(counterProvider);
 
และในส่วนของการเพิ่มค่า เมื่อกดปุ่ม เพิ่ม + ค่าก็จะทำการเพิ่มค่าในรูปแบบ
 
ref.read(counterProvider.notifier).state++;
 
และทุกครั้งที่มีการเพิ่มค่า state มีการเปลี่ยนแปลงก็จะเกิดการ rebuild ใหม่ ซึ่งตามรูปแบบที่
เราใช้งาน จะเป็นการ rebuild ใหม่ทั้งหมด วิธีการที่เราจะใช้กรณีต้องการ build ใหม่เฉพาะ
ส่วน เราจะใช้ widget ที่ชื่อ Consumer มาช่วย ตามตัวอย่างด้านล่าง
 
 
ใน Riverpod มีการใช้คำศัพท์ที่เหมือนกับที่ใช้ใน Provider สำหรับการอ่านค่า providers
 
BuildContext.watch -> WidgetRef.watch
BuildContext.read -> WidgetRef.read
BuildContext.select -> WidgetRef.watch(myProvider.select)
 
กฎการใช้งาน context.watch และ context.read ใน Provider จะนำมาใช้ใน Riverpod 
ด้วยเช่นกัน
 
ใช้ "watch" ภายในเมธอด build เพื่อให้วิดเจ็ตสามารถตรวจจับการเปลี่ยนแปลงของ provider 
และ rebuild เมื่อมีการเปลี่ยนแปลง
ใช้ "read" ภายในคลิกแฮนด์เลอร์และอีเวนต์อื่น ๆ เพื่ออ่านค่าจาก provider โดยไม่ rebuild วิดเจ็ต
ใช้ "select" เมื่อจำเป็นต้องกรองค่าหรือควบคุมการ rebuild ของวิดเจ็ตตามเงื่อนไขที่กำหนด
 
ใน Riverpod ยังมีอีกหนึ่งส่วน ที่ต่างจาก Provider ปกติ คือ ส่วนของ WidgetRef.listen
ใช้สำหรับตรวจจับการเปลี่ยนแปลงของค่า state แต่จะไม่ทำคำสั่ง rebuild  ฟังก์ชันใน listen 
สามารถใช้เพื่อจัดการกับสถานะหรือข้อผิดพลาดที่ไม่เกี่ยวข้องกับ UI
 
การกำหนดให้ rebuild เฉพาะส่วนที่เรียกใช้งาน Provider โดยเราจะใช้ widget ที่ชื่อ
Consumer และเรียกใช้ ref.watch() ในส่วนนี้แทน
 
class _HomeState extends ConsumerState<Home> {    
  @override
  Widget build(BuildContext context) {
    // Listen to the counterProvider without rebuilding the widget
    ref.listen<int>(counterProvider, (previous, next) {
      print('debug: Counter changed from $previous to $next');
    });   	  
    print("debug: build");
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        leading: IconButton(
          icon: Icon(Icons.menu),
          onPressed: () {
            Scaffold.of(context).openDrawer();
          },
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer(builder: (context, ref, child) {
              final counter = ref.watch(counterProvider);
              print("debug: build only here");
              return Text('$counter');
            })
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Increment the counter value
          ref.read(counterProvider.notifier).state++;
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
 
เราไม่ควรเรียกใช้ ref.watch ภายในฟังก์ชั่นของ ref.listen เนื่องจากจะเป็นการ rebuild วนลูป
ไม่จบสิ้นดังนั้น ให้ระวังในส่วนนี้ หากมีการใช้งาน
 
ตัวอย่างผลลัพธ์

 
 
เนื่อหาเกี่ยวกับ riverpod เบื้องต้นในตอนแรกก็ขอจบเพียงเท่านี้ ยังมีส่วนให้ทำความเข้าใจเพิ่มเติม
ในตอนต่อไป รอติดตาม


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


การใช้งาน Flutter Riverpod ร่วมกับ Provider

 
*ข้อควรรู้สำคัญ:  Flutter Riverpod คือส่วนจัดการที่ทำได้เหมือน Provider และมีความ
สามารถมากกว่า ดังนั้น เราควรต้องเลือกใช้งานอย่างใดอย่างหนึ่ง ถ้าไม่จำเป็นไม่ควรใช้ร่วมกัน
เพราะจะทำให้โครงสร้างโค้ดดูซับซ้อนอาจเกิดความสับสนได้ 
 
กรณีเราต้องการใช้งาน Flutter Riverpod กับ Provider ที่ใช้งานอยู่แล้ว ให้กำหนดดังนี้
 
ส่วนของการ import ให้กำหนดชื่อเรียกให้กับ package ก่อนเรียกใช้งาน
 
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart' as provider;
 
ในที่นี้เรากำหนดให้กับ provider ใช้ชื่อเรียกอ้างอิงเป็น provider ดังนั้นหากเรียกใช้งานฟังก์ชั่น
หรือคำสั่งใดๆ ก็ต้องระบุชื่อเรียกก่อน
 
จากนั้นกำหนดในส่วนของการเรียกใช้งานดังนี้
 
void main() {
  runApp(
    provider.MultiProvider(
      providers: [
        provider.ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: ProviderScope(
        child: MyApp(),
      ),
    ),
  );
}
 
ในที่นี้สมมติ Counter() คือ provider ที่เราเรียกใช้งาน 


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



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



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









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






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

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

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

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



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




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





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

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


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


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







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