BLoC Pattern ဆိုတာဘာလဲ?

BLoC ဆိုတာ Business Logic Component ကိုအတိုချုံ့ ပြောထားတာဖြစ်တယ်။

သူရဲ့ အဓိက ရည်ရွယ်ချက်ကတော့ Application မှာပါတဲ့ Business Logic တွေကို

  • Presentation Layer (UI) ကနေ ခွဲထုတ်နိုင်ဖို့
  • Platform/Enviornment ပေါ်မှာ မှီခိုမှု မရှိအောင် လုပ်ဖို့ပဲဖြစ်တယ်

Data layer ကသီးသန့်ဖြစ်သွားတာကြောင့် ဖြစ်နိုင်တဲ့ အခြေအနေတွေကို test လုပ်ရတာလွယ်ကူသွားပါတယ်။ User interaction တွေကလဲ bloc pattern မှာ event တွေအနေနဲ့ ဖြစ်တဲ့အတွက် Application အသုံးပြုမှုနဲ့ပတ်သက်တဲ့ ဆုံးဖြတ်ချက််တွေ ချတဲ့အခါလဲ data တွေကြည့်ပြီး ဆုံးဖြတ်ရတာ အများကြီးအထောက်အကူ ဖြစ်ပါတယ်။

Bloc ဆိုတာကို နားလည်ဖို့အတွက် Streams ကို အရင်ဆုံးနားလည်ဖို့ လိုအပ်ပါတယ်။ Stream ဆိုတာဘာလဲ ဘယ်လို အသုံးချသလဲကို ဒီမှာ သွားရောက်ပြီး ဖတ်လို့ရပါတယ်။

Streams ဆိုတာဘာလဲ?
Streams ဆိုတာကို reactive paradigm မှာ အသုံးပြုကြပါတယ်။ data အပြောင်းအလဲပေါ်မူတည်ပြီး သက်ဆိုင်ရာနေရာတွေမှာ အလိုအလျောက် ပြောင်းလဲသွားနိုင်ဖို့ တည်ဆောက်တဲ့အခါ အဓိက အသုံးပြုကြတဲ့ architecutre
  • Widget တွေက event တွေကို BLoC ဆီပို့ပေးတယ်
  • Widget တွေက BLoC ရဲ့ stream ကနေတဆင့် notification ကို လက်ခံရရှိတယ်
  • Business Logic ကို UI က ဘာမှ သိနေဖို့ မလိုအပ်တော့ဘူး

Business Logic နဲ့ UI သပ်သပ်ဆီ ဖြစ်နေခြင်းအားဖြင့်

  • Business Logic မှာ ပြောင်းလဲမှုတွေ ရှိလာရင် UI တွေကို လိုက်ပြီး ထိနေစရာမလိုတော့ဘူး
  • UI အပြောင်းအလဲလုပ်မယ်ဆိုလဲ Business Logic တွေကို ထိိဖို့မလိုတော့ဘူး
  • Business Logic ကို test လုပ်ရတာ တော်တော်လေး လွယ်ကူသွားမယ်

Flutter မှာ bloc တွေရေးဖို့အတွက်ဆို https://bloclibrary.dev/ ကို အသုံးပြုတာများတယ်။ တခြား bloc library (e.g., bloc_pattern) တွေလဲ​ ရှိပေမယ့် ဒီဟာကတော့ mature အဖြစ်ဆုံး၊ သုံးရအဆင်ပြေဆုံး industrial standard လိုမျိုး ဖြစ်နေပါပြီ။

Bloc State Management Library
Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials.

သူ့ကို အသုံးပြုမယ်ဆို နားလည်ရမယ့် Cubit နဲ့ Bloc ဆိုပြီး ၂မျိုးတော့ရှိတယ်။ ၂ခုလုံးက bloc pattern ကိုပဲ သုံးထားတာဆိုပေမယ့် အနည်းငယ်ကွာခြားမှု​ ရှိပါတယ်။

UI ကနေ Cubit/Bloc ကို ဘယ်လို ခေါ်သလဲ ဆိုတာပေါ်မူတည်ပြီး Cubit နဲ့ Bloc နဲ့ ကွာသွားတာ ဖြစ်ပါတယ်။ Business Logic တခုခုကို ခေါ်ချင်တိုင်း event တွေ ပို့နေရတာ တချို့ နေရာတွေမှာ လိုအပ်တာထက်ပိုပြီး complexity တွေများလာစေပါတယ်။ အဲ့တော့ Cubit လိုမျိုး state ကိုပြောင်းလဲဖို့ function တွေနဲ့ တန်းခေါ်တာမျိုးက သင့်တော်ပါတယ်။ ၂ခုလုံးကို package တခုထဲ ပေါင်းထားတာကြောင့် ကိုယ့်လိုအပ်ချက်ပေါ် မူတည်ပြီး သင့်တော်ရာ အသုံးပြုနိုင်ပါတယ်။

Cubit/Bloc ကို ဘယ်လို အသုံးပြုလဲ Counter App Example နဲ့ ကြည့်ကြည့်ရအောင်ပါ။ bloc သီးသန့်နဲ့ အလုပ်လုပ်ပုံ ရှင်းပြီး သွားရင် ကျွန်တော်တို့ flutter app မှာ ဘယ်လို အလုပ်လုပ်သွားသလဲ ဆက်ကြည့် သွားပါမယ်။

Cubit

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
}

ကိုယ်အသုံးပြုချင်တဲ့ class ကို Cubit ကနေ extend လုပ်ပြီး တည်ဆောက်ပေးရပါမယ်။ ဒီနေရာမှာ primitive type int ကို အသုံးပြုထားပေမယ့် အခုထက်ပိုပြီး ရှုပ်ထွေးတဲ့ value class တွေကိုလဲ ထည့်ပေးလို့ပါတယ်။ တကယ်လို့ inital values တွေကိုပါ Cubit ဆောက်တဲ့အချိန် ထည့်ပေးချင်တယ်ဆို အခုလို ရေးလို့ရပါတယ်။

class CounterCubit extends Cubit<int> {
  CounterCubit(int initialState) : super(initialState);
}
final cubitA = CounterCubit(0); // state starts at 0
final cubitB = CounterCubit(10); // state starts at 10

cubitA နဲ့ cubitB တို့က မတူညီတဲ့ initial value တွေနဲ့ instances တွေ ဖြစ်သွားမှာ ဖြစ်ပါတယ်။

အခု Cubit ရှိပြီဆိုတော့ state ကို ဘယ်လို အပြောင်းအလဲ လုပ်ကြမလဲ? Cubit မှာ state အပြောင်းအလဲကို function ကနေ လှမ်းခေါ်တယ်ဆိုတာ အပေါ်မှာ ရှင်းပြပြီးသား ဖြစ်ပါတယ်။ အဲ့တော့ counter app မှာ လိုအပ်မယ့် business logic function တွေကို cubit ထဲမှာ ထည့်သွားပါမယ်။

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  
  void decrement() => emit(state - 1);
}

state ဆိုတာကတော့ cubit ထဲမှာ ရှိနေတဲ့ data တွေရဲ့ လက်ရှိ တန်ဖိုးပဲ ဖြစ်ပါတယ်။ ဒီမှာ emit ဆိုတဲ့ function ခေါ်သွားတာကို သတိပြုမိမှာပါ။ ဒါကတော့ state ကို အပြောင်းအလဲလုပ်တဲ့ cubit function ပဲ ဖြစ်ပါတယ်။

ဒါတွေပြင်ဆင်ပြီးရင်တော့ ကျွန်တော်တို့ CounterCubit ကို အသုံးပြုလို့ရပါပြီ။

import 'package:bloc/bloc.dart';

void main() {
  final counterCubit = CounterCubit();
  print(counterCubit.state); // 0
  
  counterCubit.increment();
  print(counterCubit.state); // 1
  
  counterCubit.decrement();
  print(counterCubit.state); // 0
  
  counterCubit.close();
}

class CounterCubit extends Cubit<int> {
  CounterCubit(): super(0);
  
  increment() => emit(state + 1);
  
  decrement() => emit(state - 1);
}

Cubit ရဲ့ internal အလုပ်လုပ်တာက Stream ကို အခြေခံတာကြောင့် လုပ်စရာတွေ ပြီးသွားတဲ့ခါ stream ကို ပိတ်ပေးဖို့ close() function ကို ခေါ်ပေးရပါတယ်။

အပေါ်မှာ ပြထားတဲ့ example က state ပြောင်းလိုက် value ကို print ထုတ်လိုက် အဆင့်ဆင့် လုပ်သွားတာ ဖြစ်ပါတယ်။ တကယ့်လက်တွေ့အသုံးပြုတဲ့အခါ state အပြောင်းအလဲတွေကို တနေရာကနေ နားထောင်နေတာမျိုး အသုံးပြုလေ့ရှိပါတယ်။ ဒီတော့ အဲ့လို အခြေအနေတွက် ဘယ်လိုတည်ဆောက်လို့ ရသလဲ ကြည့်ကြည့်ရအောင်ပါ။ အရှေ့မှာ ပြောထားခဲ့သလိုပဲ Cubit ရဲ့ အလုပ်လုပ်တာက stream ကို အခြေခံတာကြောင့် အပြောင်းအလဲတွေကို နားထောင်ချင်တဲ့ နေရာတွေမှာ subscribe လုပ်ပြီး နားထောင်လို့ရပါတယ်။ အပြင်ကနေ နားထောင်တာအပြင် Cubit ထဲမှာလဲ state ပြောင်းတိုင်း trigger ဖြစ်တဲ့ onChange ဆိုတာ ရှိပါတယ်။

void main() async {
  final counterCubit = CounterCubit();
  final subscription = counterCubit.stream.listen((value) => print(value));
  
  counterCubit.increment(); // 1
  counterCubit.increment(); // 2
  counterCubit.increment(); // 3
  counterCubit.decrement(); // 2
  
  await Future.delayed(Duration.zero);
  await subscription.cancel();
  await counterCubit.close();
}
💡
await Future.delayed(Duration.zero); ကို ထည့်တာက cubit ကိုဆောက်ပြီး state change တာတွေလုပ်နေတုန်း ချက်ချင်း subscription cancel မဖြစ်သွားအောင် ထည့်ထားတာ ဖြစ်ပါတယ်။ Duration.zero ဆိုပြီး ထည့်ထားတဲ့အတွက် Event loop တခုပြီးအောင် စောင့်ခိုင်းလိုက်တဲ့ သဘောဖြစ်ပါတယ်။

State changes တွေကို နားမထောင်ချင်တော့ဘူးဆို subscription.cancel() ကိုခေါ်ပြီး နားထောင်နေတာ ရပ်လိုက်လို့ရပါတယ်။ Cubit တခုလုံးကိုပါ အသုံးမလိုတော့ဘူးဆို counterCubit.close() ဆိုပြီး ခေါ်လိုက်လို့ရပါတယ်။

Bloc

Bloc မှာ state အပြောင်းအလဲအတွက် function ခေါ်ရုံမဟုတ်ဘဲ event တွေပါ ပါလာတဲ့အတွက် Cubit နဲ့ယှဥ်ရင် နည်းနည်း ပိုပြီး ရှုပ်ထွေးပါတယ်။ ဒါပေမယ့် ဘယ်လို အသုံးပြုလို့ရသလဲ ကြည့်သွားကြရအောင်ပါ။

sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {}

final class CounterDecrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);
}

Cubit လို class တခုထဲမဟုတ်တော့ဘဲ state အပြောင်းအလဲကို event တွေက လုပ်မှာဖြစ်တဲ့အတွက် event တွေအတွက် class တွေကိုပါ တည်ဆောက်ပေးရပါတယ်။ ဒါတွေကြောင့် Cubit နဲ့ယှဥ်ရင် နည်းနည်း ပိုရှုပ်တယ်လို့ ပြောခဲ့တာပါ။ ပြီးတော့ Bloc class ထဲမှာ ပေးထားတဲ့ event တခုချင်းဆီအတွက် သက်ဆိုင်ရာ handler တွေကို တည်ဆောက်ဖို့လဲ လိုအပ်ပါတယ်။

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));

    on<CounterDecrementPressed>((event, emit) => emit(state - 1));
  }
}

ပြီးရင်တော့ bloc instance လုပ်ပြီး စတင်ပြီး သုံးလို့ရပါပြီ။ Code အစအဆုံးကို ဒီမှာ ကြည့်လို့ရပါတယ်။

import 'package:bloc/bloc.dart';

void main() async {
  final counterBloc = CounterBloc();
  print(counterBloc.state);
  
  counterBloc.add(CounterIncrementPressed());
  await Future.delayed(Duration.zero);
  print(counterBloc.state);
  
  counterBloc.add(CounterDecrementPressed());
  await Future.delayed(Duration.zero);
  print(counterBloc.state);
  
  counterBloc.close();
}

sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {}

final class CounterDecrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));

    on<CounterDecrementPressed>((event, emit) => emit(state - 1));
  }
}

Cubit လိုမျိုး function ကိုခေါ်ပြီး state ကို အပြောင်းအလဲလုပ်တာမဟုတ်ဘဲ event တွေကို ထည့်ပေးခြင်းအားဖြင့် state ကို အပြောင်းအလဲ ဖြစ်အောင် လုပ်သွားတာပဲ ဖြစ်ပါတယ်။

တကယ်လို့ state အပြောင်းအလဲကို အပေါ်မှာပြထားသလို အဆင့်ဆင့် မဟုတ်ဘဲ subscribe လုပ်ပြီး နားထောင်ချင်တာဆို အခုလို ပြင်ရေးလိုက်လို့ရပါတယ်။

void main() async {
  final counterBloc = CounterBloc();
  final subscription = counterBloc.stream.listen((value) => print(value));
  
  counterBloc.add(CounterIncrementPressed());
  await Future.delayed(Duration.zero);
  
  counterBloc.add(CounterDecrementPressed());
  await Future.delayed(Duration.zero);
  
  subscription.cancel();
  counterBloc.close();
}

Bloc Observer

Bloc ထဲမှာ ဝင်လာတဲ့ Cubit/Bloc invocations တွေကို Bloc Observer နဲ့ track လို့ရပါတယ်။ ကိုယ့် app မှာ analytics တွေအတွက် အသုံးပြုချင်တယ်ဆို အသုံးပြုလို့ရပါတယ်။ Global instance ဖြစ်တဲ့အတွက် cubits တွေ blocs တွေက ခေါ်သမျှကို ဒီထဲမှာ လက်ခံရရှိနေမှာလဲ ဖြစ်ပါတယ်။

class SimpleBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('${bloc.runtimeType} $error $stackTrace');
    super.onError(bloc, error, stackTrace);
  }
}
void main() async {
  Bloc.observer = SimpleBlocObserver();
  ...
}

အခု bloc pattern အကြောင်းကို Cubit ရော Bloc ရောနဲ့ ရှင်းပြပြီးပြီဆိုတော့ နားလည်မယ်လို့ထင်ပါတယ်။ အစမှာပြောခဲ့သလိုပဲ Business Logic တွေကို UI Layer ကနေ ခွဲထုတ်ပြီး layer တခုအနေနဲ့ state ထဲမှာ ထားထားတာမျိုး ဖြစ်ပါတယ်။​