DEV Community

loading...

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

arkarmintun1 profile image Arkar Min Tun ・3 min read

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

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

  • တခု (သို့) တခုထက်ပိုတဲ့ BLoC တွေဆီ ရွှေ့ထားဖို့
  • Presentation Layer (UI) မှာ ပါမနေစေဖို့
  • Stream အသုံးပြုပြီး input(sink), output(stream) တွေနဲ့သုံးဖို့
  • platform ပေါ်မှာ မှီခိုမှု မရှိအောင်
  • environment ပေါ်မှာ မှီခိုမှု မရှိအောင် လုပ်ထားတာဖြစ်တယ်

Image

  • Widget တွေက event တွေကို sinks တွေကနေ BLoC ဆီပို့ပေးတယ်
  • Widget တွေက BLoC ရဲ့ stream ကနေတဆင့် notification ကို လက်ခံရရှိတယ်
  • Business Logic ကို UI က ဘာမှ သိနေဖို့ မလိုအပ်တော့ဘူး

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

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

Counter Application ကိုပဲ Bloc Pattern သုံးပြီး bloc_pattern: ^2.5.1 နဲ့ ရေးကြည့်မယ်ဆိုရင် ခုလိုမျိုး ထွက်လာမယ်။

import 'dart:async';
import 'package:bloc_pattern/bloc_pattern.dart';
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: BlocProvider(
        blocs: [Bloc((i) => IncrementBloc())],
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = BlocProvider.getBloc<IncrementBloc>();

    return Scaffold(
      appBar: AppBar(
        title: Text('Counter with Stream'),
      ),
      body: Center(
        child: StreamBuilder(
          stream: bloc.outCounter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            return Text('You pressed me ${snapshot.data} times');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          bloc.incrementCounter.add(null);
        },
      ),
    );
  }
}

class IncrementBloc extends BlocBase {
  int _counter;

  //
  // Stream to handle the counter
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  // Stream to handle the action on the counter
  //
  StreamController _actionController = StreamController();
  StreamSink get incrementCounter => _actionController.sink;

  //
  // Constructor
  //
  IncrementBloc() {
    _counter = 0;
    _actionController.stream.listen(_handleLogic);
  }

  void dispose() {
    _actionController.close();
    _counterController.close();
  }

  void _handleLogic(data) {
    _counter = _counter + 1;
    _inAdd.add(_counter);
  }
}

Enter fullscreen mode Exit fullscreen mode
  1. Seperation of Responsibilities

ကြည့်မယ်ဆို CounterPage ထဲမှာ business logic လုံးဝ ပါမနေတာ တွေ့ရမယ်။ သူ့မှာ ရှိတဲ့တာဝန်က counter ကိုပြဖို့၊ value အသစ်ပြောင်းတဲ့အခါ refresh လုပ်ပေးဖို့နဲ့ button ကို နှိပ်တဲ့အခါ action ကိုပို့ပေးဖို့ပဲ ရှိတော့တယ်။

Business Logic အကုန်ကလဲ IncrementBloc ထဲမှာ တစုတစည်းထဲဖြစ်သွားတယ်။ Business Logic ပြောင်းဖို့လိုမယ်ဆို တနေရာထဲမှာ ပြောင်းပေးလိုက်ရုံပဲ ဖြစ်သွားမယ်။

  1. Testability

Business logic ကို test လုပ်ရတာလဲ ပိုမို လွယ်ကူသွားမယ်။ UI ကနေတဆင့် လုပ်နေစရာမလိုတော့ဘဲ IncrementBloc တခုကို test လုပ်ရုံနဲ့တင် ရသွားမယ်။

  1. Freedom to organize the layout

Stream ကို အသုံးပြုထားတာ ဖြစ်တဲ့အတွက် layout တွေကို ကြိုက်သလိုအပြောင်းအလဲလုပ်ရင်တောင် business logic ကို ထိမှာမျိုး မပူရတော့ဘူး။ Application ရဲ့ ကြိုက်တဲ့နေရာကနေ action ကိုခေါ်ချင်ရင် incrementCounter သုံးပြီး ခေါ်လို့ရတယ်။ ပြချင်တယ်ဆိုရင်လဲ ကြိုက်တဲ့နေရာမှာ outCounter နဲ့ ပြလို့ရတယ်။

  1. Reduction of the number of "build"s

setState() ကိုမသုံးဘဲ StreamBuilder ကိုသုံးခြင်းအားဖြင့် build လုပ်ရတဲ့ အကြိမ်အရေအတွက်ကို အများကြီး လျှော့ချပြီးသား ဖြစ်သွားတယ်။ တကယ်လိုအပ်တဲ့အချိန်မှပဲ ပြန်ပြီး build လုပ်ဖို့ လိုအပ်တယ်။ Performance ဘက်က ကြည့်မယ်ဆို ဒါဟာ ကြီးမားတဲ့ တိုးတက်မှုဖြစ်တယ်။

ဒါတွေအကုန် အလုပ်လုပ်ဖို့ဆို BLoC ကို နေရာတိုင်းက အသုံးပြုလို့ရနေဖို့လိုအပ်တယ်။ ဒီလိုလုပ်ထားဖို့ နည်းလမ်း (၃)ခုရှိတယ်။

  1. global Singleton

ဒီနည်းလမ်းက သုံးလို့ရတယ်ဆိုပေမယ့် အသုံးပြုဖို့ မတိုက်တွန်းချင်ဘူး။ Dart မှာ class destructor မရှိတဲ့အတွက် resource ထဲကနေ သူ့ကို ပြန်ထုတ်လို့ ရမှာမဟုတ်ဘူး။

  1. local instance

BLoC ကို local instance အနေနဲ့ လုပ်ပြီး သုံးလို့ရတယ်။ တချို့အခြေအနေမှာဆို ဒီလိုလုပ်ပေးတာ အသင့်တော်ဆုံးပဲ။ local instance အနေနဲ့သုံးမယ်ဆို StatefulWidget နဲ့သုံးဖို့ စဥ်းစားသင့်တယ်။ ဒီတော့မှ dispose() ထဲမှာ local instance ကို ပြန်ပြီး ဖျက်ပစ်လို့ရမယ်။

  1. provided by an ancestor

အသုံးအများဆုံးနည်းလမ်းကတော့ ကိုယ်သုံးချင်တဲ့ Widget အထက်မှာရှိတဲ့ တခုခုကနေပြီးတော့ အသုံးပြုလို့ရအောင် လုပ်ပေးထားတာမျိုးဖြစ်တယ်။

import 'package:flutter/material.dart';

// Generic Interface for all BLoCs
abstract class BlocBase {
  void dispose();
}

// Generic BLoC Provider
class BlocProvider<T extends BlocBase> extends StatefulWidget {
  final T bloc;
  final Widget child;

  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context) {
    BlocProvider<T> provider =
        context.findAncestorWidgetOfExactType<BlocProvider<T>>();
    return provider.bloc;
  }
}

class _BlocProviderState<T> extends State<BlocProvider<BlocBase>> {
  @override
  void dispose() {
    widget.bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}
Enter fullscreen mode Exit fullscreen mode

bloc_pattern library သုံးထားတဲ့နေရာမှာ ခု bloc pattern ကို သုံးလို့ရပြီ။ ခုလိုရေးတာက ancestor ကနေတဆင့် ပို့ပေးတဲ့ နည်းလမ်းကို အသုံးပြုထားခြင်းဖြစ်တယ်။

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

import 'bloc.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: BlocProvider(
        bloc: IncrementBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = BlocProvider.of<IncrementBloc>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Counter with Stream'),
      ),
      body: Center(
        child: StreamBuilder(
          stream: bloc.outCounter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            return Text('You pressed me ${snapshot.data} times');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          bloc.incrementCounter.add(null);
        },
      ),
    );
  }
}

class IncrementBloc implements BlocBase {
  int _counter;

  //
  // Stream to handle the counter
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  // Stream to handle the action on the counter
  //
  StreamController _actionController = StreamController();
  StreamSink get incrementCounter => _actionController.sink;

  //
  // Constructor
  //
  IncrementBloc() {
    _counter = 0;
    _actionController.stream.listen(_handleLogic);
  }

  void dispose() {
    _actionController.close();
    _counterController.close();
  }

  void _handleLogic(data) {
    _counter = _counter + 1;
    _inAdd.add(_counter);
  }
}
Enter fullscreen mode Exit fullscreen mode

BlocProvider ကို ဘယ်လို လုပ်သလဲဆို

home: BlocProvider(
  bloc: IncrementBloc(),
  child: CounterPage(),
),
Enter fullscreen mode Exit fullscreen mode

ဒီနေရာမှာ provider တခုကို တည်ဆောက်ပြီး အဲ့ဒီ့ provider ကနေ ပို့ပေးမယ့် bloc (IncrementBloc) ကို တည်ဆောက်တယ်။ IncrementBloc() ကို သုံးမယ့် CounterPage() ကိုလဲ child ထဲမှာ တခါထဲ ကြေငြာသွားတယ်။

ဒီလို ကြေငြာပြီးတာနဲ့ BlocProvider အောက်မှာ ရှိတဲ့ sub-tree အကုန်လုံးကနေ ကြိုက်တဲ့နေရာမှာ IncrementBloc ကို ယူသုံးလို့ရပြီ။

BLoC တွေအများကြီးကို သုံးလို့ရသလားဆိုရင်တော့ ရတယ်လို့ပဲ ဖြေပေးရမယ်။ အသုံးပြုဖို့ တိုက်တွန်းချင်တဲ့ နေရာတွေကတော့

  1. Business logic ရှိတယ်ဆိုတဲ့ page တိုင်းရဲ့ ထိပ်ဆုံးမှာ
  2. Application State ကို handle လုပ်ဖို့ ApplicationBloc ဆိုပြီး သုံးတဲ့နေရာမှာ
  3. Component တခုအနေနဲ့ အတိုင်းအတာတခုထိ ရှုပ်ထွေးလာပြီဆို သက်ဆိုင်ရာ BLoC သုံးပြီး လုပ်တဲ့နေရာတွေမှာ အသုံးပြုပေးပါ။

Discussion

pic
Editor guide