ข้ามไปยังเนื้อหาหลัก
คู่มือระบบ เนื้อหาผ่านการตรวจสอบแล้ว

การใช้ BLoC Pattern สำหรับ State Management

บทความนี้แนะนำ BLoC Pattern ซึ่งเป็นสถาปัตยกรรมสำหรับการจัดการ State ใน Flutter โดยแยก Business Logic ออกจาก UI เพื่อให้โค้ดมีความเป็นระเบียบ, ทดสอบได้ง่าย, และ...

การใช้ BLoC Pattern สำหรับ State Management ใน Flutter

ในบทความก่อนหน้า เราได้สำรวจการจัดการ Dependency และ State ด้วย GetX ซึ่งเป็น microframework ที่เน้นความรวดเร็วและเรียบง่าย ในบทความนี้ เราจะมาเจาะลึกอีกหนึ่ง pattern ที่ได้รับความนิยมอย่างมากในการจัดการ State ในแอปพลิเคชัน Flutter ขนาดใหญ่ นั่นคือ BLoC (Business Logic Component) Pattern

BLoC Pattern เป็นสถาปัตยกรรมที่แยกส่วน Business Logic ออกจาก UI (User Interface) ทำให้ code ของเรามีความเป็นระเบียบ, สามารถทดสอบได้ง่าย, และ maintainable มากขึ้น โดย BLoC จะทำหน้าที่เป็นตัวกลางระหว่าง UI และ Data Layer รับ Events จาก UI, ประมวลผล Business Logic, และส่ง States กลับไปยัง UI เพื่ออัปเดต

BLoC Pattern ประกอบไปด้วยองค์ประกอบหลัก 3 ส่วน:

  • Events: เหตุการณ์ที่เกิดขึ้นจาก UI หรือส่วนอื่นๆ ของแอปพลิเคชัน เช่น การกดปุ่ม, การโหลดข้อมูล, หรือการเปลี่ยนแปลงใน input field
  • States: สถานะของ UI ที่เปลี่ยนแปลงไปตาม Events ที่เกิดขึ้น เช่น Loading, Loaded, Error, หรือ Empty
  • Bloc: ตัวกลางที่รับ Events, ประมวลผล Business Logic, และส่ง States กลับไปยัง UI

ทำไมต้องใช้ BLoC Pattern?

BLoC Pattern มีข้อดีหลายประการที่ทำให้เป็นตัวเลือกที่ดีสำหรับการจัดการ State ในแอปพลิเคชันขนาดใหญ่:

  • Separation of Concerns: แยก Business Logic ออกจาก UI อย่างชัดเจน ทำให้ code อ่านง่าย, เข้าใจง่าย, และแก้ไขได้ง่าย
  • Testability: Business Logic สามารถทดสอบได้อย่างอิสระ โดยไม่ต้องพึ่งพา UI
  • Reusability: BLoC สามารถนำไปใช้ซ้ำในหลายๆ ส่วนของแอปพลิเคชัน
  • Maintainability: เมื่อ Business Logic เปลี่ยนแปลง, เราสามารถแก้ไขเฉพาะใน BLoC โดยไม่ต้องแก้ไข UI
  • Predictability: States ที่ส่งออกมาจาก BLoC จะเป็นไปตาม Events ที่เข้ามา ทำให้ UI สามารถคาดการณ์ผลลัพธ์ได้

Events

Events คือสัญญาณที่ส่งจาก UI ไปยัง BLoC เพื่อบอกว่ามีบางอย่างเกิดขึ้น ตัวอย่างเช่น หากเรามีหน้าจอสำหรับแสดงรายการสินค้า เราอาจมี Events ดังนี้:

  • LoadProductsEvent: Event ที่บอกให้ BLoC โหลดรายการสินค้า
  • RefreshProductsEvent: Event ที่บอกให้ BLoC รีเฟรชรายการสินค้า
  • AddProductEvent: Event ที่บอกให้ BLoC เพิ่มสินค้าใหม่

เราสามารถสร้าง Events เป็น abstract class และสร้าง subclasses สำหรับแต่ละ Event ได้ดังนี้:

abstract class ProductEvent {}

class LoadProductsEvent extends ProductEvent {}

class RefreshProductsEvent extends ProductEvent {}

class AddProductEvent extends ProductEvent {
  final String productName;
  final double productPrice;

  AddProductEvent({required this.productName, required this.productPrice});
}

States

States คือสถานะของ UI ที่เปลี่ยนแปลงไปตาม Events ที่เกิดขึ้น ตัวอย่างเช่น หากเรามีหน้าจอสำหรับแสดงรายการสินค้า เราอาจมี States ดังนี้:

  • ProductLoadingState: State ที่บอกว่ากำลังโหลดรายการสินค้า
  • ProductLoadedState: State ที่บอกว่าโหลดรายการสินค้าสำเร็จแล้ว
  • ProductErrorState: State ที่บอกว่าเกิดข้อผิดพลาดในการโหลดรายการสินค้า
  • ProductEmptyState: State ที่บอกว่าไม่มีรายการสินค้า

เราสามารถสร้าง States เป็น abstract class และสร้าง subclasses สำหรับแต่ละ State ได้ดังนี้:

abstract class ProductState {}

class ProductLoadingState extends ProductState {}

class ProductLoadedState extends ProductState {
  final List<Product> products;

  ProductLoadedState({required this.products});
}

class ProductErrorState extends ProductState {
  final String errorMessage;

  ProductErrorState({required this.errorMessage});
}

class ProductEmptyState extends ProductState {}

Bloc

Bloc คือหัวใจหลักของ BLoC Pattern ทำหน้าที่รับ Events, ประมวลผล Business Logic, และส่ง States กลับไปยัง UI

เราสามารถสร้าง Bloc โดย extends `Bloc` class จาก `flutter_bloc` package

ตัวอย่างเช่น:

import 'package:flutter_bloc/flutter_bloc.dart';

class ProductBloc extends Bloc&ltProductEvent, ProductState&gt {
  final ProductRepository productRepository;

  ProductBloc({required this.productRepository}) : super(ProductLoadingState()) {
    on<LoadProductsEvent>((event, emit) async {
      emit(ProductLoadingState());
      try {
        final products = await productRepository.getProducts();
        if (products.isEmpty) {
          emit(ProductEmptyState());
        } else {
          emit(ProductLoadedState(products: products));
        }
      } catch (e) {
        emit(ProductErrorState(errorMessage: e.toString()));
      }
    });

    on<RefreshProductsEvent>((event, emit) async {
      // Implement refresh logic here
      emit(ProductLoadingState());
      try {
        final products = await productRepository.getProducts();
        if (products.isEmpty) {
          emit(ProductEmptyState());
        } else {
          emit(ProductLoadedState(products: products));
        }
      } catch (e) {
        emit(ProductErrorState(errorMessage: e.toString()));
      }
    });

    on<AddProductEvent>((event, emit) async {
      // Implement add product logic here
      try {
        await productRepository.addProduct(event.productName, event.productPrice);
        // After adding, reload the products
        add(LoadProductsEvent()); // Dispatch LoadProductsEvent to reload
      } catch (e) {
        emit(ProductErrorState(errorMessage: "Error adding product: ${e.toString()}"));
      }
    });
  }
}

// Assuming ProductRepository is a class that handles data fetching
class ProductRepository {
  Future<List<Product>> getProducts() async {
    // Simulate fetching products from an API
    await Future.delayed(Duration(seconds: 2));
    return [
      Product(name: "Product 1", price: 10.0),
      Product(name: "Product 2", price: 20.0),
    ];
  }

  Future<void> addProduct(String name, double price) async {
    // Simulate adding a product to a database or API
    await Future.delayed(Duration(seconds: 1));
    print("Adding product: $name with price: $price");
  }
}

class Product {
  final String name;
  final double price;

  Product({required this.name, required this.price});
}

ในโค้ดข้างต้น:

  • `ProductBloc` extends `Bloc<ProductEvent, ProductState>` ซึ่งระบุว่า Bloc นี้จะรับ `ProductEvent` และส่ง `ProductState`
  • Constructor รับ `ProductRepository` ซึ่งเป็น class ที่ทำหน้าที่จัดการการดึงข้อมูลสินค้า
  • `on((event, emit) async {})` ใช้สำหรับจัดการแต่ละ Event ที่เข้ามา
  • ภายใน `on` เราจะประมวลผล Business Logic และส่ง States กลับไปยัง UI โดยใช้ `emit()`

BlocProvider

`BlocProvider` เป็น Widget ที่ใช้สำหรับ inject BLoC เข้าไปใน Widget Tree ทำให้ Widget ต่างๆ สามารถเข้าถึง BLoC ได้

เราสามารถใช้ `BlocProvider` ได้ดังนี้:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter BLoC Demo',
      home: BlocProvider(
        create: (context) => ProductBloc(productRepository: ProductRepository()),
        child: ProductScreen(),
      ),
    );
  }
}

class ProductScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final productBloc = BlocProvider.of<ProductBloc>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Products'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: () {
              productBloc.add(RefreshProductsEvent());
            },
          ),
        ],
      ),
      body: BlocBuilder<ProductBloc, ProductState>(
        builder: (context, state) {
          if (state is ProductLoadingState) {
            return Center(child: CircularProgressIndicator());
          } else if (state is ProductLoadedState) {
            return ListView.builder(
              itemCount: state.products.length,
              itemBuilder: (context, index) {
                final product = state.products[index];
                return ListTile(
                  title: Text(product.name),
                  subtitle: Text('Price: \$${product.price}'),
                );
              },
            );
          } else if (state is ProductErrorState) {
            return Center(child: Text('Error: ${state.errorMessage}'));
          } else if (state is ProductEmptyState) {
            return Center(child: Text('No products found.'));
          } else {
            return Center(child: Text('Unknown state'));
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _showAddProductDialog(context, productBloc);
        },
        child: Icon(Icons.add),
      ),
    );
  }

  void _showAddProductDialog(BuildContext context, ProductBloc productBloc) {
    String productName = '';
    double productPrice = 0.0;

    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Add Product'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                decoration: InputDecoration(labelText: 'Product Name'),
                onChanged: (value) {
                  productName = value;
                },
              ),
              TextField(
                decoration: InputDecoration(labelText: 'Product Price'),
                keyboardType: TextInputType.numberWithOptions(decimal: true),
                onChanged: (value) {
                  productPrice = double.tryParse(value) ?? 0.0;
                },
              ),
            ],
          ),
          actions: [
            TextButton(
              child: Text('Cancel'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('Add'),
              onPressed: () {
                productBloc.add(AddProductEvent(productName: productName, productPrice: productPrice));
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

ในโค้ดข้างต้น:

  • เราใช้ `BlocProvider` เพื่อ inject `ProductBloc` เข้าไปใน `ProductScreen`
  • ใน `ProductScreen` เราสามารถเข้าถึง `ProductBloc` ได้โดยใช้ `BlocProvider.of(context)`
  • เราใช้ `BlocBuilder` เพื่อ rebuild Widget เมื่อ State ของ `ProductBloc` เปลี่ยนแปลง
  • เรา dispatch `LoadProductsEvent` เมื่อหน้าจอโหลดครั้งแรก และ dispatch `RefreshProductsEvent` เมื่อผู้ใช้กดปุ่ม Refresh

ข้อผิดพลาดที่พบบ่อย

  • ลืม inject BLoC ด้วย BlocProvider: หากลืม inject BLoC, Widget จะไม่สามารถเข้าถึง BLoC ได้
  • ไม่ได้จัดการทุก State ใน UI: หากไม่ได้จัดการทุก State ใน UI, UI อาจแสดงผลข้อมูลที่ไม่ถูกต้อง
  • dispatch Event ใน Build Method: การ dispatch Event ใน Build Method จะทำให้เกิด infinite loop
  • ไม่ close BLoC: เมื่อ BLoC ไม่ได้ใช้แล้ว, ควร close BLoC เพื่อป้องกัน memory leak

สรุป

BLoC Pattern เป็นสถาปัตยกรรมที่ทรงพลังสำหรับการจัดการ State ในแอปพลิเคชัน Flutter ขนาดใหญ่ โดยแยก Business Logic ออกจาก UI ทำให้ code ของเรามีความเป็นระเบียบ, สามารถทดสอบได้ง่าย, และ maintainable มากขึ้น

ในบทความถัดไป เราจะสำรวจอีกหนึ่ง library ที่ได้รับความนิยมในการจัดการ State นั่นคือ Riverpod

Office.in.th Custom Service

ต้องการพัฒนาระบบหรือปรับแต่งจากโครงสร้างนี้?

Office.in.th รับพัฒนาและออกแบบ Enterprise Web Application, API Integration และระบบบริหารงานภาครัฐ/เอกชน พร้อมการสนับสนุนระยะยาว

ดูบริการรับพัฒนาซอฟต์แวร์