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
- 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.
- States là output của một
- 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)
- Cubit sẽ thông báo có state mới thông qua lệnh
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àmonChange
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 khistate
của cubit được cập nhật. Mộtchange
bao gồmcurrentState
vànextState
-
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.
- 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>
- Bloc yêu cầu đăng kí các event thông qua hàm
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.
- Trong Bloc,
- 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); }
- Quan sát State : Giống với cubit, bloc cũng quán sát các state thay đổi thông quan
- 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ởionChange
@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àmadd
để thêm một event của bloc.
- Bloc cung cấp hàm
- 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ọiaddError
,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); }
- Giống với cubit, bloc cũng có hàm
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 :