รู้จักและใช้งาน Modifiers ใน Flutter Riverpod เบื้องต้น ตอนที่ 3

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

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

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


เนื้อหาตอนต่อไปนี้ เรามาดูกันต่อเกี่ยวกับ Riverpod จะมาดูในส่วนของ
Modifiers หรือการปรับแต่งการใช้งานของการกำหนด Provider ซึ่งใน
Riverpod จะมีให้ใช้งาน อยู่ 2 แบบคือ autoDispose และ family ตาม
ที่ได้แนะนำเบื้องต้นไปแล้วในตอนแรก ของบทความ Riverpod

*ข้อควรรู้สำคัญ: Flutter Riverpod คือส่วนจัดการที่ทำได้เหมือน Provider และมีความ
สามารถมากกว่า ดังนั้น เราควรต้องเลือกใช้งานอย่างใดอย่างหนึ่ง ถ้าไม่จำเป็นไม่ควรใช้ร่วมกัน
เพราะจะทำให้โครงสร้างโค้ดดูซับซ้อนอาจเกิดความสับสนได้ 
 
ทบทวนบทความก่อนหน้าได้ที่ 
ทำความรู้จักและใช้งาน Riverpod ใน Flutter เบื้องต้น ตอนที่ 1 http://niik.in/1107
รู้จักและใช้งาน AsyncValue ใน Flutter Riverpod เบื้องต้น ตอนที่ 2 http://niik.in/1108
 

การใช้งาน Provider Modifiers 

    ปกติแล้วเมื่อมีการใช้งาน Riverpod จัดการข้อมูล State ผ่าน Provider ข้อมูลจะถูก cache
เพื่อให้สามารถใช้งานผ่านส่วนต่างๆได้ และไม่มีการล้างค่าและลบออกค่าสถานะของ state อัตโนมัติ
ซึ่งถ้าแอปของเราไม่ได้มีหลายส่วน ที่แยกกันไม่เกี่ยวข้องกัน หรือทั้งแอปมีส่วนที่เกี่ยวข้องกันทั้งหมด
แค่ส่วนเดียว เราอาจจะไม่จำเป็นต้องพิจารณาส่วนนี้ก็ได้ เพื่อให้แอปทำงานได้เต็มประสิทธิภาพ และใช้
การล้างค่า แบบกำหนดเองแทนได้  อย่างไรก็ดี ส่วนใหญ่แล้วแอปจะมีหลายๆ ส่วนที่แยกกัน ดังนั้น
การเลือกใช้การล้างค่า cache อัตโนมัติ จึงเป็นวิธีที่เหมาะสม ป้องกันการรั่วไหลของการใช้งานหน่วย
ความจำ ทำให้เกิดป้ญหาได้ ตัว modifier ของ Riverpod ที่จัดการในส่วนนี้คือ autoDispose
 
    autoDispose: จะทำการล้างแคชอัตโนมัติเมื่อ provider ไม่ถูกใช้งาน
    family: ช่วยให้สามารถส่งอาร์กิวเมนต์ไปยัง provider ได้
 
    สำหรับ family เป็น modifier อีกตัว ที่เราสามารถใช้เพื่อส่งค่าอาร์กิวเมนต์เข้าไปดำเนินการ
ในส่วนจัดการ state  และหากมีการใช้งาน family ก็ย่อมทำให้เกิด provider ที่มีค่าที่เปลียน
แปลงไปตามอาร์กิวเมนต์ที่ส่งเข้าไป ทำให้มีการอ้างอิง provider จำนวนมาก เสี่ยงต่อการเกิดปัญหา
การใช้งานหน่วยความจำ ดังนั้น หากมีการใช้งาน family ก็มักจะใช้งานร่วมกับ autoDispose 
เสมอ เพื่อป้องกันปัญหานี้
 
     ดูตัวอย่างการกำหนด provider ข้างล่าง 2 อันนี้
 
// 1 provider แบบฟังก์ชั่น
final activityProvider = FutureProvider.autoDispose((ref) async {
  // TODO: perform a network request to fetch an activity
  return fetchActivity();
});

// 2 แบบ class extends จาก notifier อีกที
final activityProvider2 = AsyncNotifierProvider<ActivityNotifier, Activity>(
  ActivityNotifier.new,
);

class ActivityNotifier extends AsyncNotifier<Activity> {
  @override
  Future<Activity> build() async {
    // TODO: perform a network request to fetch an activity
    return fetchActivity();
  }
}
 
    อย่างที่ได้อธิบายไปในตอนต้นๆ หากเราต้องการให้สามารถปรับแต่งที่มากขึ้น เราสามารถใช้
ในรูปแบบ class ได้ ในตัวอย่างข้างต้น แบบแรก มีการใช้งาน modifier แบบ autoDispose 
นั้นคือหาก provider ไม่ถูกเรียกใช้งาน หรือมีการเปลี่ยนไปหน้าที่ไม่ได้ใช้งาน provider นี้แล้ว
ค่า cache ของ provider จะถูกล้างค่าและลบออกไปอัตโนมัติ
    ในตัวอย่างที่ 2 ไม่มีการใช้งาน modifier แต่แบบที่สองนี้ จะขออธิบายเพิ่มเติม เกี่ยวกับการ
เลือกใช้งาน class ที่จะ extends มาใช้งาน ดังนี้
 
ถ้าใช้ Provider ->  ควรทำการ extends จากคลาส
NotifierProvider -> Notifier
AsyncNotifierProvider -> AsyncNotifier
AsyncNotifierProvider.autoDispose -> AutoDisposeAsyncNotifier
AsyncNotifierProvider.autoDispose.family -> AutoDisposeFamilyAsyncNotifier
 
    ตัวอย่างที่ 2 ใช้แบบ AsyncNotifierProvider จึงต้องทำการ extends จาก  AsyncNotifier
รูปแบบการใช้งาน AsyncNotifierProvider.autoDispose.family คือมีการใช้งาน modifier
ทั้ง autoDispose และ family
    ถ้ามี Async ให้จำไว้ว่า เป็นการดำเนินการที่ต้องมีเวลาที่ต้องรอเข้ามาเกี่ยวข้อง เช่น ดึงข้อมูลจาก
File หรือจาก Serer ผ่าน API เหล่านี้เป็นต้น
 
    หากมีการใช้งาน autoDispose จะไม่ซับซ้อนอะไร เป็นการกำหนดให้ สามารถล้างค่า cache หาก
ไม่ได้ใช้งานแล้วอัตโนมัติ แต่สำหรับ family จะเป็นกำหนดให้สามารถ ส่งค่าตัวแปรเข้าไปได้ ดังนั้น
จึงสามารถปรับได้เป็นดังนี้
 
// 1 provider แบบฟังก์ชั่น
final activityProvider = FutureProvider.autoDispose
  .family<Activity, String>((ref, activityType) async {
  // รับค่า activityType ส่งเข้าไปใช้งานในฟังก์ชั่น fetchActivity
  return fetchActivity(activityType);
});

// 2 แบบ class extends จาก notifier อีกที
final activityProvider2 = AsyncNotifierProvider.autoDispose
.family<ActivityNotifier, Activity, String>(
  ActivityNotifier.new,
);

class ActivityNotifier extends AutoDisposeFamilyAsyncNotifier<Activity, String>
{
  @override
  Future<Activity> build(String activityType) async {
    // สามารถดูค่า อาร์กิวเมนต์ผ่านการเรียกใช้งาน "this.arg"
    print(this.arg);
    // รับค่า activityType ส่งเข้าไปใช้งานในฟังก์ชั่น fetchActivity
    return fetchActivity(activityType);
  }
}
 
    ตอนนี้เราเห็นภาพการใช้งานภาพรวมเบื้องต้นแล้ว จะขอยกตัวอย่าง อธิบายเพิ่มเติมจากตอนที่แล้ว
เราจะปรับโค้ดที่ดึงข้อมูลจำลอง  เป็นดังนี้ เพื่อใช้งาน autoDispose modifier
 
// ตัวอย่างการกำหนด
final productProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
  ref.onCancel((){
    print("debug: No one listens to me anymore!");
  });
  ref.onDispose((){
    print("debug: If I've been defined as `.autoDispose`, I just got disposed!");
  });  
  print("debug: run provider");
  const urlApi = "https://www.ninenik.com/demo/api/simpleproduct";
  final response = await http.get(Uri.parse(urlApi));
  return parseProducts(response.body);
});
 
    กรณี provider ข้างต้น เราสามารถำหนด autoDispose เพิ่มเข้าไปได้เลย และไม่ต้องทำอะไร
เมื่อไม่มีการใช้งาน ค่า cache ของ provider จะถูกทำลายและลบออกไปเอง 
    แต่ถ้าเราอยากให้สามารถกำหนดหรือเปรับแต่งเพิ่มขึ้น สามารถปรับมาใช้ตัว AsyncNotifierProvider.autoDispose แทนดังนี้
 
class ProductNotifier extends AutoDisposeAsyncNotifier<List<Product>> {
  @override
  Future<List<Product>> build() async {

    ref.onCancel((){
      print("debug: No one listens to me anymore!");
    });
    ref.onDispose((){
      print("debug: If I've been defined as `.autoDispose`, I just got disposed!");
    });
    const urlApi = "https://www.ninenik.com/demo/api/simpleproduct";
    final response = await http.get(Uri.parse(urlApi));
    return parseProducts(response.body);
  }
}

final productProvider = AsyncNotifierProvider.autoDispose<ProductNotifier, List<Product>>(
  ProductNotifier.new,
);
 
    การทำงานของ ref.onCancel((){}); จะเกิดขึ้นทุกครั้งเมื่อเราเปลี่ยนไปหน้าอื่่น 
ส่วน ref.onDispose((){}); จะเกิดขึ้นก็ต่อเมื่อเรามีการใช้งาน autoDispose modifier และมี
การเปลี่ยนไปหน้าอื่นหรือเรียกข้อมูลใหม่ เราสามารถกำหนดให้ทำงานบางอย่างที่ต้องการในส่วนนี้ได้หากจำเป็น
 
    ต่อไป เราจะจำลองการใช้งาน family modifier เราจะทำการส่งค่า รูปแบบการจัดเรียงข้อมูล เดิม
รายการสินค้าของเราแสดงราคาจากน้อยไปมาก เราจะให้สามารถส่งค่าอาร์กิวเมนต์ที่เลือกได้ว่าจะเรียง
ราคาจากมากไปน้อย หรือน้อยไปมาก เพื่อให้ provider แสดงข้อมูลตามที่เราต้องการ
 

ไฟล์ home.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;

// Data models 
class Product {
  int id;
  String title;
  num price;
  DateTime accessDate;
 
  Product({
    required this.id,
    required this.title,
    required this.price,
    required this.accessDate,
  });
 
  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'] as int,
      title: json['title'] as String,
      price: json['price'] as num,
      accessDate: DateTime.parse(json['accessDate'] as String),
    );
  }
}
 
List<Product> parseProducts(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
  return parsed.map<Product>((json) => Product.fromJson(json)).toList();
}  
 
// ตัวอย่างการกำหนด
final productProvider = FutureProvider.autoDispose
.family<List<Product>, String>((ref, [sortType ='asc']) async {
  print("debug: run provider");
  const urlApi = "https://www.ninenik.com/demo/api/simpleproduct";
  final response = await http.get(Uri.parse(urlApi));
  // ส่งกลับข้อมูลไปแสดง เอาแค่ 20 รายการ
    List<Product> productAll = parseProducts(response.body).take(20).toList();
    // ทดสอบแสดงข้อมูล ก่อนเรียง
    productAll.forEach((product) => print('debug: ${product.title}: ${product.price}'));
    if(sortType=='asc'){
      print("debug: sort asc");
      productAll.sort((a, b) => a.price.compareTo(b.price));       
    }else{
      print("debug: sort desc");
      productAll.sort((a, b) => b.price.compareTo(a.price));     
    }
    // ทดสอบแสดงข้อมูล หลังเรียง
    productAll.forEach((product) => print('debug: ${product.title}: ${product.price}'));
    return productAll; 
});

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> {
  String _sortType = 'asc'; // กำหนดค่าเริ่มต้น

  @override
  Widget build(BuildContext context) {
    final productAsyncValue = ref.watch(productProvider(_sortType));
    print("debug: build");
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        leading: IconButton(
          icon: Icon(Icons.menu),
          onPressed: () {
            Scaffold.of(context).openDrawer();
          },
        ),
        actions: [      
          IconButton(
            onPressed: () {
              // สลับค่าการเรียง 'asc' และ 'desc'
              setState(() {
                _sortType = _sortType == 'asc' ? 'desc' : 'asc';
              });
              ref.refresh(productProvider(_sortType).future);
            },
            icon: Icon(Icons.sort_outlined),
          ),          
          IconButton(
            onPressed: () {
              ref.refresh(productProvider(_sortType).future);
            },
            icon: Icon(Icons.refresh_outlined),
          ),
        ],        
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          ref.refresh(productProvider(_sortType).future);
        },
        child: productAsyncValue.when(
          data: (products) { // เมื่อมีข้อมูลสินค้ากลับมา วนลูปสร้างikpdki
            return ListView.builder(
              itemCount: products.length,
              itemBuilder: (context, index) {
                final product = products[index];
                return ListTile(
                  title: Text(product.title),
                  subtitle: Text('$${product.price}'),
                  trailing: Text('${product.accessDate.toString().substring(0,19)}'),
                );
              },
            );
          },
          loading: () => Center(child: CircularProgressIndicator()),
          error: (error, stackTrace) => Center(child: Text('Error: $error')),
        ),
      ),
    );
  }
}
 
    การใช้งาน ทั้ง autoDispose และ family จะทำให้เราสามารถส่งข้อมูล String กำหนดรูปแบบ
การเรียงข้อมูลเข้าไปจัดรูปแบบรายการก่อนส่งกลับมาแสดงได้ ในตัวอย่างเราใช้ state ชื่อ _sortType
เข้ามาช่วย เวลาเรียกใช้งาน ก็จะต้องปรับเป็นดังนี้
 
final productAsyncValue = ref.watch(productProvider(_sortType));
// เดิมกรณีไม่ได้ส่งค่าอาร์กิวเมนต์เข้าไป จะเป็นลักษณะนี้
// final productAsynValue = ref.watch(productProvider);
 
มีการส่ง _sortType เข้าไปใน provider เป็นคุณสมบัติของ family modifier  นอกจากนั้นสิ่ง
ที่เราจะสังเกตเห็นได้ชัดคือ รายการข้อมูลจะมีจังหวะแสดงตัวโหลดข้อมูลทุกครั้งที่มีการไปหน้าอื่นแล้ว
ย้อนกลับมาหน้ารายการ ทั้งนี้เพราะมีการล้างค่าทุกครั้งที่ไม่ได้ใช้งาน
 
    ต่อไปลองปรับรูปแบบจาก FutureProvider มาเป็นแบบ class แทนจะได้เป็นดังนี้
 
class ProductNotifier extends AutoDisposeFamilyAsyncNotifier<List<Product>, String> {
  @override
  Future<List<Product>> build(String sortType) async {
    const urlApi = "https://www.ninenik.com/demo/api/simpleproduct";
    final response = await http.get(Uri.parse(urlApi));
    // ส่งกลับข้อมูลไปแสดง เอาแค่ 20 รายการ
    List<Product> productAll = parseProducts(response.body).take(20).toList();
    // ทดสอบแสดงข้อมูล ก่อนเรียง
    productAll.forEach((product) => print('debug: ${product.title}: ${product.price}'));
    if(this.arg=='asc'){
      print("debug: sort asc");
      productAll.sort((a, b) => a.price.compareTo(b.price));   
    }else{
      print("debug: sort desc");
      productAll.sort((a, b) => b.price.compareTo(a.price));         
    }
    // ทดสอบแสดงข้อมูล หลังเรียง
    productAll.forEach((product) => print('debug: ${product.title}: ${product.price}'));
    return productAll;
  }
}

final productProvider = AsyncNotifierProvider.autoDispose
.family<ProductNotifier, List<Product>, String>(
  ProductNotifier.new,
);
 
    โค้ดส่วนอื่นยังเหมือนเดิม เปลี่ยนแค่รูปแบบมาใช้เป็นแบบ class แทนก็จะได้ผลลัพธ์เหมือนกัน
ก่อนจบ เนื้อหาเกี่ยวกับ Flutter Riverpod ตัวอย่างอีกเล็กน้อย สมมติเราต้องการกำหนด state
การจัดเรียงข้อมูล ให้สามารถคงค่าเดิมไว้ แม้ออกไปแล้วกลับมาก็ยังให้ใช้ค่าการเรียงล่าสุดที่เลือก
สามารถกำหนดเป็นดังนี้แทนได้
 
// สร้าง StateProvider สำหรับเก็บ sortType
final sortTypeProvider = StateProvider<String>((ref) => 'asc');
 
และในส่วนของคำสั่ง build ก็ดึงค่ามาใช้งาน
 
  @override
  Widget build(BuildContext context) {
    // อ่านค่า sortType จาก provider
    final _sortType = ref.watch(sortTypeProvider);

    final productAsyncValue = ref.watch(productProvider(_sortType));
    print("debug: build");
    return Scaffold(
      appBar: AppBar(
 
และในส่วนของการกดปุ่มเพื่อเปลี่ยนค่า ก็ปรับเป็นดังนี้
 
IconButton(
  onPressed: () {
    // สลับค่าการเรียง 'asc' และ 'desc'
    final _newSortType = _sortType == 'asc' ? 'desc' : 'asc';
    ref.read(sortTypeProvider.notifier).state = _newSortType;              
    ref.refresh(productProvider(_sortType).future);
  },
  icon: Icon(Icons.sort_outlined),
),  
 
    แบบนี้เราก็จะสามารถคงค่ารูปแบบการเรียงข้อมูลให้คงไว้จนกว่าจะปิดแอปไปได้
 
เนื้อหาโดยภาพรวมของ Flutter Riverpod ก็ขอจบลงเพียงเท่านี้ สามารถศึกษาเพิ่มเติมได้
ที่ลิ้งค์หมายเหตุด้านล่าง ประยุกต์ปรับใช้ให้เหมาะสมตามต้องการ
    ในตอนหน้าเนื้อหาจะเกี่ยวกับอะไรใหม่ รอติดตาม


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



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



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









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






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

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

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

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



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




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





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

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


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


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







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