atomic_flutter state management

State Management နဲ့ ပတ်သက်ရင် flutter မှာ Provider, Bloc, Riverpod, GetX, Redux စသဖြင့် နာမည်ရပြီးသား state management တွေ အများအပြားရှိပါတယ်။ သူတို့ကို လက်ရှိ production ထိသွားနေတဲ့ application တွေမှာလဲ အသုံးပြုနေကြတာ ဖြစ်လို့ ယုံယုံကြည်ကြည်နဲ့ အသုံးပြုနိုင်တယ်။ အကုန်လုံးမှာ သူ့အားနည်းချက် အားသာချက် အသီးသီးနဲ့ တကယ်လဲ အသုံးဝင်ပါတယ်။ အားနည်းချက် အားသာချက်ဆိုတာထက် ကိုက်ညီတဲ့နေရာ မကိုက်ညီတဲ့နေရာလို့ပြောရင် ပိုမှန်မယ်။ ကိုယ်ရေးမယ့် application နဲ့ ကိုက်ညီတဲ့ state management ကို အသုံးပြုတာကလဲ project timeline ပေါ် အများကြီး သက်ရောက်မှုရှိတယ်။

Bloc တို့ Redux တို့ကျတော့ နားလည်ရမယ့် Stream တို့, Flux တို့ concept တွေနဲ့ biloilerplate တွေများတယ်။ Provider ဆိုရင်လဲ state ကို widget tree ရဲ့ တစိတ်တပိုင်းအဖြစ် သိမ်းထားတာကို နားလည်ဖို့လိုတယ်။ Riverpod ဆိုလဲ သူ့မှာပါတဲ့ read/watch concept တွေနဲ့ state တွေ notifier တွေ အဆင့်ဆင့် depend ဖြစ်တာကို သတိထားရတယ်။

ကျွန်တော့်အတွက်တော့ state management တခု အသုံးပြုချင်တိုင်း သူ့ဆီက လုပ်ပုံလုပ်နည်း အဆင့်ဆင့်တွေ၊ syntax တွေကို အချိန်ပေးပြီး ပြန်လေ့လာနေရတာ သိပ်ပြီး အဆင်မပြေလှဘူး။ Project တွေဆိုတာကလဲ အမြဲ green field မဟုတ်တော့ ကိုယ်အသားကျပြီးသား state management ကို ပြောင်းချင်တိုင်း ကောက်ပြောင်းလို့ မရပြန်ဘူး။ ဒါ့အပြင် state management solution တွေကလဲ အချိန်ကြာလာတာနဲ့အမျှ feature တွေ ထပ်ထပ်ထည့်နေကြတာဆိုတော့ ပိုပိုပြီး ရှုပ်ထွေးလာတယ်။ State management ကို သုံးရချင်း အဓိက ရည်ရွယ်ချက်က runtime မှာ data တွေကို သိမ်းထားပြီး နေရာအမျိုးမျိုးက ယူသုံးနိုင်ဖို့ပဲ။ ဒီတော့ over engineering ဖြစ်တာကို ဘယ်လိုကိုင်တွယ်လို့ ရနိုင်မလဲ စဥ်းစားရင်း atomic_flutter ကို စမ်းရေးကြည့်ဖြစ်သွားတယ်။

Inspiration

ဒီ package ရဲ့ inspiration က atomic design ကို ယူထားတာပါ။ state တွေကို atom တွေလို့ သဘောထားတာကို အခြေခံပါတယ်။ Simple အဖြစ်ဆုံး string တို့, number တို့ကနေပြီးတော့ complex ဖြစ်တဲ့ user-defined type တွေအထိ atom တွေအနေနဲ့ state တွေ တည်ဆောက်နိုင်တယ်။ ကျွန်တော်တို့ သိမ်းချင်တဲ့ application ရဲ့ အချက်အလက် အစိတ်အပိုင်းတွေကို atom တွေအဖြစ် ခွဲပြီး သိမ်းတဲ့ သဘောပေါ့။ အသုံးမလိုတော့တဲ့ atom တွေကိုလဲ အလိုအလျောက် dispose လုပ်ပေးသွားပါလိမ့်မယ်။

example: Typical e-commerce app with atoms

Basic Setup

Atom

atomic_flutter ရဲ့ အခြေခံအကျဆုံး state ကိုပြပါဆိုရင် အခုလို တွေ့ရလိမ့်မယ်။

final counterAtom = Atom<int>(0);

Atom class ကနေပြီး counterAtom ဆိုတဲ့ Object တခုကို တည်ဆောက်လိုက်တာပဲ ဖြစ်ပါတယ်။ Initial value အနေနဲ့ 0 ကို ထည့်ထားပေးတယ်။

String အနေနဲ့ atom တည်ဆောက်ချင်ရင်လဲ အခုလို ဆောက်လိုက်လို့ရတယ်။

final nameAtom = Atom<String>('', id: 'nameAtom');

id ကတော့ မထည့်ပေးလဲရပါတယ်။ ဘယ် state ပြောင်းသွားတာလဲ debug လုပ်ချင်တဲ့အခါမျိုးမှာ အသုံးပြုလို့ရအောင် ထည့်ပေးထားတာပါ။ မထည့်ပေးတဲ့အခါ auto generated လုပ်ထားတဲ့ id တွေကို သုံးနေမှာပဲ ဖြစ်ပါတယ်။

flutter: Atomic: Disposing atom atom_1237
flutter: Atomic: Decremented ref count for atom atom_11601: 1
flutter: Atomic: Decremented ref count for atom atom_11601: 0
flutter: Atomic: Scheduling dispose for atom atom_11601 in 120 seconds
flutter: Atomic: Decremented ref count for atom cart: 0
flutter: Atomic: Decremented ref count for atom atom_2011: 1
flutter: Atomic: Decremented ref count for atom atom_2011: 0
flutter: Atomic: Scheduling dispose for atom atom_2011 in 120 seconds

computed

တကယ်လို့ atom တခုနဲ့တခုချိတ်ဆက်ဖို့လိုအပ်ရင်လဲ ပေါင်းပြီးတော့ atom အသစ်တွေ လုပ်လို့ရတယ်။ နဂိုမူက atom တွေပြောင်းတိုင်းမှာ derived atom ကလဲ လိုက်ပြောင်းပေးနေမှာပဲ ဖြစ်ပါတယ်။

// Define primary atoms
final priceAtom = Atom<double>(10.0);
final quantityAtom = Atom<int>(2);

// Create a computed atom that depends on the other atoms
final totalAtom = computed<double>(
  () => priceAtom.value * quantityAtom.value,
  tracked: [priceAtom, quantityAtom],
  id: 'totalPrice',
);

atom ထဲက သိမ်းထားတဲ့ value ကို အသုံးပြုချင်တဲ့အခါ အခုလိုပဲ အလွယ်တကူ အသုံးပြုလို့ရပါတယ်။

// Read the current value
int count = counterAtom.value;

အခု atom တည်ဆောက်တာတွေ အတော်အသင့် နားလည်ပြီဆိုတော့ atom ထဲက value တွေကို ဘယ်လို ပြောင်းလဲမလဲ ဆက်ကြည့်ကြည့်ရအောင်ပါ။

set, update, batch

atom တွေမှာ set, update နဲ့ batch ဆိုပြီး function ၃ခု ရှိပါတယ်။

// Update to new value
counterAtom.set(5);

// Update based on the current value
counterAtom.update((current) => current + 1);

// Batch multiple updates to prevent intermediate rebuilds
counterAtom.batch(() {
  counterAtom.set(0);
  nameAtom.set('New User');
});
  • set - value အသစ်ကို ပြောင်းလဲချင်တဲ့အခါ အသုံးပြုနိုင်ပါတယ်။
  • update - လက်ရှိ value ပေါ် အခြေခံပြီးတော့မှ ပြောင်းလဲချင်တဲ့အခါ အသုံးပြုနိုင်ပါတယ်။
  • batch - state ပြောင်းတိုင်း rendering တွေ ခဏခဏမလုပ်ဘဲ တခါထဲ ပေါင်းလုပ်ချင်တဲ့အခါ အသုံးပြုနိုင်ပါတယ်။

ဒီနေရာမှာ atom ထဲက value ကို တိုက်ရိုက် အပြောင်းအလဲ မလုပ်မိအောင်၊ set တို့ update တို့နဲ့ပဲ ပြောင်းလဲနိုင်အောင် encapsulate လုပ်ထားတာဖြစ်ပါတယ်။ counterAtom.value++ ဆိုပြီးတော့ အသုံးပြုလို့မရပါဘူး။

Domain-Specific Atom

atom တွေတည်ဆောက်တဲ့အချိန်မှာ သူတို့နဲ့ ပတ်သက်တဲ့ domain logic တွေကိုပါ အတူတွဲပြီး တည်ဆောက်ချင်တဲ့အခါ domain-sepcific atom အနေနဲ့လဲ တည်ဆောက်လို့ရပါတယ်။ ဥပမာ - counter atom အတွက်ဆို increment, decrement, reset function တွေကို counter နဲ့ဆိုင်တဲ့ domain logic တွေလို့ မြင်လို့ရပါတယ်။ cart atom ဆိုရင်လဲ add to cart, remove from cart, reset cart စတာတွေကို သူနဲ့ဆိုင်တဲ့ domain logic တွေလို့ မြင်လို့ရပါတယ်။

class CounterAtom extends Atom<int> {
  CounterAtom() : super(0, id: 'counter', autoDispose: false);

  void increment() {
    update((current) => current + 1);
  }

  void decrement() {
    update((current) => current - 1);
  }

  void reset() {
    set(0);
  }
}

// Usage
final counterAtom = CounterAtom();
counterAtom.increment(); // 1
counterAtom.increment(); // 2
counterAtom.increment(); // 3
counterAtom.decrement(); // 2
counterAtom.reset(); // 0
class CartAtom extends Atom<Cart> {
  CartAtom() : super(const Cart(), id: 'cart', autoDispose: false);

  void addProduct(Product product, int quantity) {
    update((cart) => cart.addItem(
      CartItem(product: product, quantity: quantity)
    ));
  }

  void removeProduct(int productId) {
    update((cart) => cart.removeItem(productId));
  }

  bool hasProduct(int productId) {
    return value.items.any((item) => item.product.id == productId);
  }
}

// Usage
final cartAtom = CartAtom();
cartAtom.addProduct(product, 2);

domain logic တွေကို တစုတစည်းထဲ ထားခြင်းက logic တွေ ပြင်ဖို့ ပြောင်းဖို့ လိုအပ်တဲ့အခါ အလွယ်တကူ ပြင်လို့ ပြောင်းလို့ ရတာပဲ ဖြစ်ပါတယ်။

Setup ပိုင်းတွေ ပြီးပြီဆိုတော့ Flutter app တည်ဆောက်တဲ့အချိန် widget tree ထဲမှာ atoms တွေကို ဘယ်လို အသုံးပြုနိုင်လဲ ဆက်ကြည့်လိုက်ရအောင်ပါ။

Widget Usages

AtomBuilder

ပထမဆုံး widget ကတော့ atom value တွေပြောင်းတိုင်းမှာ re-render လုပ်ဖို့ရာ အသုံးပြုနိုင်တဲ့ AtomBuilder ပဲဖြစ်ပါတယ်။

AtomBuilder(
  atom: counterAtom,
  builder: (context, count) {
    return Text('Count: $count');
  },
);

အပေါ်မှာ ပြထားသလိုပဲ ကျွန်တော်တို့ အသုံးပြုချင်တဲ့ atom ကိုထည့်ပေးလိုက်တာနဲ့ အဲ့ဒီ့ atom ပြောင်းလဲတိုင်းမှာ builder method ကိုခေါ်ပြီးတော့ widget အသစ်ကို render လုပ်ပေးသွားမှာပဲ ဖြစ်ပါတယ်။

MultiAtomBuilder

တကယ်လို့ widget က တခုထက်ပိုတဲ့ atom တွေ ပြောင်းလဲတဲ့အချိန်တိုင်းမှာ re-render လုပ်ဖို့လိုရင်တော့ MultiAtomBuilder ကို အသုံးပြုလို့ရပါတယ်။

MultiAtomBuilder(
  atoms: [userAtom, themeAtom],
  builder: (context) {
    final user = userAtom.value;
    final theme = themeAtom.value;

    return Text('Hello ${user.name}', style: theme.textStyle);
  },
);

အပေါ်မှာ ပြထားတဲ့ widget ကတော့ userAtom နဲ့ themeAtom ပြောင်းလဲတိုင်းမှာ re-render လုပ်ပေးမှာပဲ ဖြစ်ပါတယ်။

AtomSelector

တကယ်လို့ ကျွန်တော်တို့က domain specific atom လိုမျိုး တည်ဆောက်ထားတဲ့အခါ atom တခုလုံး ပြောင်းလဲတိုင်းမှာ မဟုတ်ဘဲ atom ရဲ့ အစိတ်အပိုင်းတခု ပြောင်းလဲတော့မှ re-render လုပ်တာမျိုးလဲ လိုအပ်နိုင်ပါတယ်။​ ဒီလိုအခြေအနေမှာတော့ AtomSelector ကို အသုံးပြုနိုင်ပါတယ်။

AtomSelector<UserProfile, String>(
  atom: userProfileAtom,
  selector: (profile) => profile.name,
  builder: (context, name) {
    return Text('Name: $name');
  },
);

အပေါ်မှာ ပြထားတဲ့ widget ကတော့ userProfileAtom ပြောင်းလဲတိုင်းမှာ render လုပ်မှာ မဟုတ်ဘဲ userProfileAtom ထဲကမှ name ပြောင်းလဲသွားတော့မှ re-render လုပ်မှာပဲ ဖြစ်ပါတယ်။

ဒါတွေကို နားလည်ပြီဆိုရင်တော့ atomic_flutter package ကို စတင်အသုံးပြုလို့ ရပြီပဲ ဖြစ်ပါတယ်။ ပိုပြီး complex ဖြစ်တဲ့ အသုံးပြုပုံတွေနဲ့ extension အကြောင်းတွေကိုတော့ နောက် article တွေမှာ ပြောပြပေးသွားပါ့မယ်။