layout: page
tags: [flutter]
title: BLOC 
subtitle: State management với Bloc

Nguồn : Bloc State Management Library

I. TỔNG QUAN
  • Bloc hỗ trợ nhiều framwork, bài viết này mình viết cho flutter nên chúng ta chỉ cần quan tâm đến 2 package :
    • bloc - Core Bloc Library
    • flutter_bloc : Cung cấp các widget để làm việc với bloc
II. TẠI SAO DÙNG BLOC ?
  • Bloc được thiết kế với 3 giá trị cốt lỗi Simple, Powerfull, Testable
    • Simple : Dễ hiểu, developer ở trình nào cũng có thể dùng được.
    • Powerfull : Trợ giúp tuyệt với, từ app phức tạp đến app đơn giản.
    • Testable : Dễ dàng test
III. Một Số Khái Niệm Quan Trọng
  • Streams : Để làm việc với bloc bạn phải biết cách sử dụng [Stream]([Asynchronous programming: Streams Dart](https://dart.dev/tutorials/language/streams))

Stream là một dữ liệu bất đồng bộ liên tiếp. Đễ dễ hiệu stream giống như ống nước, còn dữ liệu chính là dòng nước chảy liên tục bên trong.

  • Đẩy dữ liệu vào Stream :
// Stream<int> : Luồng dữ liệu có loại dữ liệu là int.
// async* : keyword này để cho biết bên trong hàm có thể dùng yield
// yield : Dùng để đẩy dữ liệu vào Stream.
Stream<int> countStream(int max) async* {
    for (int i = 0; i < max; i++) {
        yield i;
    }
}
  • Lấy dữ liệu từ Stream :
//Hàm này có tham số là một Stream với kiểu dữ liệu là int.
//async : để đánh dấu đây là hàm bất đồng bộ.
//await : async - await luôn đi chung với nhau
// => Khi dùng async bạn phải dùng await.

Future<int> sumStream(Stream<int> stream) async {
    int sum = 0;
    await for (int value in stream) {
        sum += value;
    }
    return sum;
}
3.1. Cubit
  • Là một đối tượng dùng để quản lý sate. Nó là subclass của BlocBase và có thể được mở rộng để quản lý bất kì loại state nào

s

  • Cubit cung cấp một số hàm mà sẽ được gọi khi state thay đổi.
    • States là output của một cubit và nó đại diện một phần state app của bạn. UI sẽ được thôn báo state và thực hiện vẽ lại chính nó dựa vào state.
  • Tạo một cubit :
    • Bước 1: Tạo một subclass của Cubit
    • Bước 2 : Chỉ định kiểu dữ liệu cho Cubit (Ví dụ : cubit kiểu int Cubit<int>)
    • Bước 3: Chỉ định giá trị khởi tạo. (Ví dụ : Khởi tạo giá trị 0 super(0))
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
}
  • Thay đổi State :
    • Cubit sẽ thông báo có state mới thông qua lệnh emit (emit là protected, nó chỉ dùng được ở bên trong cubit)
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  //emit đùng để thông báo cubit có state mới
  void increment() => emit(state + 1);
}
  • Dùng Cubit :
    • Dùng cubit cơ bản :
    void main() {
      //1. Khởi tạo cubit
      final cubit = CounterCubit();
    
      //2. 
      // - Truy cập giá trị state hiện tại của cubit
      // - state có kiểu dữ liệu phụ thuộc vào kiẻu dữ liệu của cubit
      print(cubit.state); // 0
    
      //3. Cập nhật state
      cubit.increment();
      print(cubit.state);
    
      //4. Đóng cubit khi không dùng
      cubit.close(); 
    }
    
    • Stream
    Future<void> main() async {
    
      //1. Khởi tạo cubit
      final cubit = CounterCubit();
    
      //2. Lắng nghe cubit cập nhật state
      final subscription = cubit.stream.listen(print); 
    
      //3. Khi increment được gọi , callback của listen sẽ được gọi
      cubit.increment();
    
      // Thêm dòng delay này để tránh cubit nó huỷ quá sớm
      await Future.delayed(Duration.zero);
    
      // 4. Cancel, không lắng nghe stream của cubit
      await subscription.cancel();
    
      // 5. Không dùng nữa thì đóng. 
      await cubit.close();
    }
    
  • Quan sát Cubit : Khi cubit thông báo có state mới, một Change sẽ xảy ra. Chúng ta có thể quan sát tất cả thay đổi thông qua hàm onChange

    class CounterCubit extends Cubit<int> {
      CounterCubit() : super(0);
    
      void increment() => emit(state + 1);
    
      @override
      void onChange(Change<int> change) {
        super.onChange(change);
        print(change);
      }
    }
    
  • onChange sẽ được gọi trước khi state của cubit được cập nhật. Một change bao gồm currentStatenextState
  • Error Handling : Mỗi cubit có hàm addError dùng để thông báo khi có lỗi xảy ra.

    class CounterCubit extends Cubit<int> {
      CounterCubit() : super(0);
    
      void increment() {
        //1. Thêm error.
        addError(Exception('increment error!'), StackTrace.current);
        emit(state + 1);
      }
    
      @override
      void onChange(Change<int> change) {
        super.onChange(change);
        print(change);
      }
    
      @override
      void onError(Object error, StackTrace stackTrace) {
        //2. Khi một error được thêm, hàm onError sẽ được gọi
        print('$error, $stackTrace');
        super.onError(error, stackTrace);
      }
    }
    
SUMMARY CUBIT
  • Dùng để quản lý state.
  • Lệnh emit để thông báo khi có state mới.
  • Quan sát state thay đổi bằng cách override onChange
  • addError dùng để thêm một lỗi
  • Handle các lỗi bằng cách override onError
3.2. Bloc
  • Bloc là class nâng cao hơn, nó sẽ phản hồi state thay đổi thông quan các event. Bloc cũng kế thừa từ BlocBase.

ss

  • Tạo một Bloc :
    • Bước 1 : Tạo một subclass kế thừa Bloc.
    • Bước 2 : Chỉ định event và state cho Bloc (Ví dụ : Bloc<CounterEvent, int>)
    • Bước 3 : Khỏi tạo state cho Bloc (Ví dụ : super(0))
sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {}

//Tạo một bloc giống như tạo một cubit. Tuy nhiên :
// + Bloc chỉ định thêm một thành phần là  Event (Ví dụ : CounterEvent)
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);
  • Thay đổi State :
    • Bloc yêu cầu đăng kí các event thông qua hàm on<Event>
sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) {
      // handle incoming `CounterIncrementPressed` event
      //emit chỉ hoạt động bên trong event.
    });
  }
}
  • Note :
    • Trong Bloc, emit chỉ có tác dụng khi bạn gọi nó bên trong xử lý của event.
    • Bloc và cubit sẽ bỏ qua các state giống nhau. Nếu emit state mới mà state cũ == sate mới thì state không thay đổi. Nghĩa là event không được kích hoạt.
  • Dùng Bloc :
    • Dùng bloc cơ bản :
    Future<void> main() async {
      //Bước 1: Khởi tạo
      final bloc = CounterBloc();
      print(bloc.state); 
      // Bước 2 : Dùng hàm Add, lúc này event được mà bạn đăng sẽ xảy ra.
      bloc.add(CounterIncrementPressed());
    
      await Future.delayed(Duration.zero);
      print(bloc.state); // 1
      // Bước 3: Đóng bloc.
      await bloc.close();
    }
    
    • Dùng với Stream :
    Future<void> main() async {
      //Bước 1: Khởi tạo
      final bloc = CounterBloc();
      // Bước 2: Đắng kí lắng nghe stream
      final subscription = bloc.stream.listen(print); 
      // Bước 3 : 
      // + Dùng hàm Add, lúc này event được mà bạn đăng sẽ xảy ra.
      // + Bên trong xử lý event có gọi emit thì hàm bạn truyền
      //vào listen sẽ được gọi
      bloc.add(CounterIncrementPressed());
      await Future.delayed(Duration.zero);
      // Bước 4 : Huỷ không lắng nghe stream.
      await subscription.cancel();
      // Bước 5: Đóng bloc.
      await bloc.close();
    }
    
  • Quan sát Bloc :
    • Quan sát State : Giống với cubit, bloc cũng quán sát các state thay đổi thông quan onChange
    @override
      void onChange(Change<int> change) {
        super.onChange(change);
        print(change);
    }
    
  • Tuy nhiên, dùng onChange chúng thông không thể biết được state thay đổi thông qua event nào. Vì vậy Bloc cũng hỗ trợ một hàm khác đó là onTransition. Nó bao gồm các thông tin như : state hiện tại, event, state tiếp theo.
  • onTransition được gọi khi thay đổi từ một state này sang state khác. Nó sẽ gọi bởi onChange

    @override
      void onTransition(Transition<CounterEvent, int> transition) {
        super.onTransition(transition);
        print(transition);
    }
    
  • Quán sát Event :
    • Bloc cung cấp hàm onEnvent để thông quán sát các event được thêm vào bloc. Nghĩa là nó sẽ được gọi khi bạn gọi hàm add để thêm một event của bloc.
  • Error Handling :
    • Giống với cubit, bloc cũng có hàm addError để thông báo khi có một lỗi xảy ra. Khi bạn gọi addError, onError sẽ được gọi giúp bạn quan sát hoặc xử lý logic nào đó khi lỗi xảy ra.
    addError(Exception('increment error!'), StackTrace.current); 
    
    @override
    void onError(Object error, StackTrace stackTrace) {
        print('$error, $stackTrace');
        super.onError(error, stackTrace);
    }
    
3.3 Cubit và Bloc
  • Bây giờ chúng ta so sánh và cân nhấc khi nào dùng cubit, khi nào dùng bloc.
  • Counter Cubit :
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}
  • Counter Bloc :
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }
}
  • Chúng ta thấy cubit ngắn gọn hơn, không cần phải khai báo thêm class event. Chúng ta cũng có thể gọi emit bất cứ khi nào
  • Bloc Advantages :
    • Một trong các lợi thế lớn của bloc là nó cho biết trình từ các state thay đổi chính xác.
    • Ví dụ, bạn muốn quản lý AuthenticationState. Đơn giản, chúng ta tạo một enum bao gồm các giá trị :
enum AuthenticationState { unknown, authenticated, unauthenticated }
  • Trong app của bạn, AuthenticationState có thể thay đổi giá trị trong từng thời điểm, khi dùng bloc bạn sẽ biết được :
    • State trước đó là gì
    • State mới là gì
    • Event nào làm State thay đổi. Nghĩa là xử lý nào trong App làm State của bạn thay đổi.
Transition {
  currentState: AuthenticationState.authenticated,
  event: LogoutRequested,
  nextState: AuthenticationState.unauthenticated
}
  • Thay vào đó, nếu dùng cubit, chúng ta chỉ biết được
    • State trước đó là gì
    • State mới là gì
  • Chúng ta không biết được điều gì làm state thay đổi.

Advanced Event Transformations :

TBD :