Design Pattern รูปแบบ หรือสไตล์การเขียนโปรแกรม มีความสำคัญอย่างมากเมื่อเราอยากสร้างโปรเจคที่สามารถนำไปต่อยอดได้ ดูแลรักษาได้ และเพิ่มประสิทธิภาพได้
BLoC pattern หรือ Business Logic of Component คือ Design Pattern ที่มีแนวคิดของการแยก Logic ออกจากส่วนแสดงผล หรือพูดอีกนัยนึงคือ เราพยายามแยกให้โค๊ดอันนึงทำหน้าที่คำนวนตรรกะต่างๆ แล้วอีกอันก็ประกอบไปด้วยการแสดงผล ซึ่งให้อารมณ์คล้ายๆ JavaScript กับ Html แต่เรานำมาใช้บน Flutter นั่นเอง
ด้วยหลักการของ BLoC จากเดิมที่แอปหน้านึงจะมีเพียง 1 ไฟล์ เราก็ต้องเพิ่มไฟล์ของ BLoC เพิ่มเข้าไปด้วย เพราะเราต้องแยกส่วนของหลักการออกมา ดังนั้น จากจุดนี้ไปก็จะพบว่า BloC จะมีความยากระดับนึง แล้วอาจจะ เกินความจำเป็น สำหรับแอปที่ไม่ได้มีความซับซ้อนมาก แต่ก็คุ้มค่า ถ้าแอปเราเริ่มมีขนาดระดับนึง หรือเมื่อต้องส่ง state ไปมาระหว่างหน้า
เนื่องจาก BLoC เป็นเพียง Pattern
ดังนั้นเราสามารถเลือก State Management ที่จะใช้ได้ ซึ่งโดยทั่วไปก็จะนิยมใช้ Stream กับ flutter_bloc ในบทความนี้จะเลือกใช้
flutter_bloc
เพราะเริ่มมีความนิยมมากกว่า
ก่อนจะเริ่มเอา BLoC มาใช้ ลองมาดูโครงสร้างของมันกันก่อน
จากรูปข้างบน จะเห็น flow ของข้อมูล โดย bloc เองก็จะทำหน้าที่เป็นตัวกลางของข้อมูล จะไม่ได้ไปมีส่วนในการเรนเดอร์ UI ส่วนของ UI เองก็จะไม่ได้มี logic ข้างใน แต่จะรับข้อมูลมาจาก BLoC แทน หรือถ้าจะเรียกฟังก์ชั่นก็จะเรียกผ่าน BLoC เช่นกัน
⚠️⚠️⚠️ (อัพเดท 8/7/2020) ถ้าใครใช้ flutter_bloc เวอร์ชั่นตั้งแต่ 1.0.0 ขึ้นไป จะมีการเปลี่ยนชื่อ Api บางส่วนดังนี้
bloc.state.listen -> bloc.listen
bloc.currentState -> bloc.state
bloc.dispatch -> bloc.add
bloc.dispose -> bloc.close
ดังนั้นตอนทำตาม อาจใช้เวอร์ชั่นที่ระบุไว้ไปก่อนได้ หลักการทำงานของ flutter_bloc ยังเหมือนเดิม
เริ่มเขียน Flutter ด้วย BLoC Pattern กัน
ก่อนที่เราจะเริ่มเขียน BLoC Pattern แบบจริงจัง ลองมาทำความคุ้นเคยกับคำสั่งต่างๆที่เราต้องใช้กันก่อน ฉะนั้น มาลองแก้ โปรแกรมแรกเริ่ม Increment app ให้เป็น BLoC Pattern กัน
เตรียมความพร้อม
flutter create increment_app
หลังจากนั้นก็ทำการแก้ไขไฟล์
pubspec.yml
โดยก็อปด้านล่าง หรือ เพิ่ม
flutter_bloc: ^0.18.3
equatable: ^0.2.0
เข้าไปในส่วน
dependencies:
name : increment_app
description : A new Flutter project.
version : 1.0.0+1
environment :
sdk : " >=2.1.0 <3.0.0"
dependencies :
flutter :
sdk : flutter
flutter_bloc : ^0.18.3 # เรียกเพื่อใช้ state management ของ flutter_bloc เพื่อใช้
equatable : ^0.2.0 # ใช้ประกอบกับ flutter_bloc
cupertino_icons : ^0.1.2
dev_dependencies :
flutter_test :
sdk : flutter
flutter :
uses-material-design : true
แล้วอัพเดท dependencies โดยรัน
flutter packages get
เป็นอันจบขั้นตอนการเตรียมความพร้อม
เริ่มแก้ไขโค๊ดกัน
เราจะเริ่มต้นด้วยการสร้าง bloc เพื่อรองรับการใช้งานของส่วน UI กันก่อน โดยตอนนี้ Logic เรามีเพียงแสดงตัวเลข กับ เพิ่มค่าของเลขเมื่อกดปุ่มบวก ตามเดิมใน
main.dart
เราจะพบว่าค่าของเลขถูกเก็บใน
_MyHomePageState
และถูกอัพเดทโดยฟังก์ชั่น
_incrementCounter()
โดยเรียก
setState()
เพื่ออัพเดทการแสดงผลของตัวเลข
เราจะทำการย้าย logic ออกมา แล้วเปลี่ยนไปใช้ state management ของ
flutter_bloc
แทน โดยเริ่มต้นดังต่อไปนี้
สร้างโฟลเดอร์สำหรับ จัดการ logic ของเรา
increment_app/lib /**bloc**
แล้วสร้างไฟล์สำหรับ bloc หน้า main ของเราจำนวน 4 ไฟล์ดังนี้
increment_app/lib/bloc /**counter_event.dart**
รับ event จากส่วน UI
increment_app/lib/bloc /**counter_state.dart**
อัพเดทข้อมูลส่วน UI
increment_app/lib/bloc /**counter_bloc.dart**
logic ที่รับข้อมูลจาก counter_event.dart
ประมวลผล แล้วส่งต่อไปยัง counter_state.dart
increment_app/lib/bloc /**counter.dart**
export ไฟล์ข้างต้นทั้ง 3 เพื่อความง่ายในการเรียกใช้งาน
ก่อนอื่น มารวมไฟล์เพื่อให้ export ได้ง่าย ที่ **counter.dart**
กันก่อน
export 'counter_bloc.dart' ;
export 'counter_event.dart' ;
export 'counter_state.dart' ;
เริ่มต้นที่ counter_event.dart
import package meta
สำหรับใช้เรียก
@immutable
และ
equatable
สำหรับใช้เปรียบเทียบ state ในอดีตกับปัจจุบัน เพื่อให้
flutter_bloc
สามารถเช็ค แล้วอัพเดทเฉพาะค่าที่จำเป็นได้
import 'package:meta/meta.dart' ;
import 'package:equatable/equatable.dart' ;
สร้าง abstract class CounterEvent โดยที่ขยายมาจาก
Equatable
ที่เรา import มา
@immutable
abstract class CounterEvent extends Equatable {
CounterEvent([ List props = const []]) : super (props);
}
สร้าง class IncrementCounter ที่ทำหน้าที่ส่งต่อตัวเลขไปยัง bloc
class IncrementCounter extends CounterEvent {
final int counter;
IncrementCounter( this .counter) : super ([counter]);
@override
String toString() => 'IncrementCounter {counter : $counter }' ;
}
รวมกันทั้งหมด จะได้โค๊ดดังนี้
import 'package:meta/meta.dart' ;
import 'package:equatable/equatable.dart' ;
@immutable
abstract class CounterEvent extends Equatable {
CounterEvent ([List props = const []]) : super (props);
}
class IncrementCounter extends CounterEvent {
final int counter;
IncrementCounter (this .counter) : super ([counter]);
@override
String toString () => 'IncrementCounter {counter : \$counter }' ;
}
มาต่อกันที่ counter_state.dart
Import meta กับ equatable เข้ามา
import 'package:meta/meta.dart' ;
import 'package:equatable/equatable.dart' ;
สร้าง abstract class CounterState
@immutable
abstract class CounterState extends Equatable {
CounterState([ List props = const []]) : super (props);
}
สร้าง class UpdateCounterState สำหรับส่งค่า counter ให้ UI นำไปใช้งาน
class UpdateCounterState extends CounterState {
final int counter;
UpdateCounterState( this .counter): super ([counter]);
@override
String toString() {
return 'UpdateCounterState { counter: $counter }' ;
}
}
import 'package:meta/meta.dart' ;
import 'package:equatable/equatable.dart' ;
@immutable
abstract class CounterState extends Equatable {
CounterState ([List props = const []]) : super (props);
}
class UpdateCounterState extends CounterState {
final int counter;
UpdateCounterState (this .counter): super ([counter]);
@override
String toString () {
return 'UpdateCounterState { counter: \$counter }' ;
}
}
หลังจากเราได้สร้าง event กับ state เรียบร้อยแล้ว เรามาต่อกันที่ bloc ต่อ ซึ่งทำหน้าที่รับค่าจาก event แล้วประมวลผลตามประเภทของ event แล้วส่งค่าต่อไปยัง state
ส่วน logic ของเรา counter_bloc.dart
import async
กับ
bloc
เข้ามา และก็นำ event กับ state เข้ามาด้วย โดยเรียกผ่าน
counter.dart
import 'dart:async' ;
import 'package:bloc/bloc.dart' ;
import './counter.dart' ;
ต่อมา สร้าง class CounterBloc ที่เราจะขยายมาจาก Bloc
<CounterEvent, CounterState>
โดยต้องระบุ
event และ
state ที่นำมาใช้ ซึ่งคือ
CounterEvent กับ
CounterState
ในส่วนนี้แนะนำให้ก็อปโค๊ดด้านล่างมาก่อน แล้วเราจะมาอธิบายส่วนต่างๆ กัน
import 'dart:async' ;
import 'package:bloc/bloc.dart' ;
import './counter.dart' ;
class CounterBloc extends Bloc <CounterEvent , CounterState > {
@override
CounterState get initialState { //--> 1*
return UpdateCounterState (0 );
}
@override
Stream <CounterState > mapEventToState ( //--> 2*
CounterEvent event,
) async * {
if (event is IncrementCounter ) {
yield * _mapIncrementCountertoState (event);
}
}
Stream <CounterState > _mapIncrementCountertoState (IncrementCounter event) async * { //--> 3*
final int counter = event.counter + 1 ;
yield UpdateCounterState (counter);
}
}
ในตัวอย่างนี้อาจจะทำให้สงสัยได้ว่า จะมี 2* ไปทำไม ถ้าเรามีแค่ event อันเดียว และ state อันเดียว ที่เราใส่ไปเช่นนี้ไว้ก่อนเพราะในความเป็นจริงแล้ว เราจะมี event และ state มากกว่า 1 อัน เช่นเราอาจจะเพิ่มปุ่มลดตัวเลขทีละ 1 เข้าไป เราก็ต้องเพิ่ม event อีกอัน
*เดี๋ยวจะลองเพิ่มส่วนนี้ในภายหลัง
ตอนนี้ เราได้ทำในส่วนของ bloc เรียบร้อยแล้ว ต่อไปนี้เราก็สามารถเรียกใช้มันได้แล้ว
มาที่ main.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,
),
home: MyHomePage (title: 'Flutter Demo Home Page' ),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage ({Key key, this .title}) : super (key: key);
final String title;
@override
_MyHomePageState createState () => _MyHomePageState ();
}
class _MyHomePageState extends State <MyHomePage > {
int _counter = 0 ;
void _incrementCounter () {
setState (() {
_counter++ ;
});
}
@override
Widget build (BuildContext context) {
return Scaffold (
appBar: AppBar (
title: Text (widget.title),
),
body: Center (
child: Column (
mainAxisAlignment: MainAxisAlignment .center,
children: < Widget > [
Text (
'You have pushed the button this many times:' ,
),
Text (
'\$_counter ' ,
style: Theme .of (context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton (
onPressed: _incrementCounter,
tooltip: 'Increment' ,
child: Icon (Icons .add),
),
);
}
}
เพิ่ม
counter.dart
กับ
flutter_bloc.dart
เข้ามา
import 'package:bloc_test/bloc/counter.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
แก้
main()
ให้
BlocProvider
มาครอบ
MyApp()
ไว้ เพื่อให้เราสามารถเรียก Bloc ของเราจากหน้าไหนของแอปก็ได้ โดยต้องระบุ Bloc ที่เราจะใช้ด้วย ซึ่งคือ
CounterBloc()
/* void main() => runApp(MyApp()); */
void main () => runApp (BlocProvider (
builder: (BuildContext context) => CounterBloc (),
child: MyApp (),
));
ตอนนี้แอปเราพร้อมที่จะเรียกใช้ bloc แล้ว
เราก็จะมาแก้ในส่วน
class _MyHomePageState extends State < MyHomePage >
ที่ซึ่งเดิมยังประกอบไปด้วยค่านับเลข
_counter
และ logic
_incrementCounter()
เราจะแทนที่สิ่งเหล่านี้ด้วย bloc ที่เราสร้าง
@override
Widget build (BuildContext context) {
final counterBloc = BlocProvider .of <CounterBloc >(context); // เรียกใช้ CounterBloc
return BlocBuilder ( // เมื่อสร้าง widget ที่ต้องการนำค่าจาก bloc มาใช้ ให้นำ BlocBuilder มาคลุม
bloc: counterBloc, // ระบุ counterBloc ที่เราสร้าง
builder: (BuildContext context, CounterState state) {
final counter = (state as UpdateCounterState ).counter; // ค่านับเลขถูกเรียกจาก state โดยเราจะระบุ state ที่นำมาใช้งานด้วย เพื่อให้ complier เราสามารถเข้าใจว่ามีตัวแปรใดบ้างในคลาสของ state นี้
return Scaffold (
appBar: AppBar (
title: Text (widget.title),
),
body: Center (
child: Column (
mainAxisAlignment: MainAxisAlignment .center,
children: < Widget > [
Text (
'You have pushed the button this many times:' ,
),
Text (
/* '\$_counter', */
'\$counter ' , // เราเรียก counter มาใช้งานได้เลย
style: Theme .of (context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton (
onPressed: () => counterBloc.dispatch (IncrementCounter (counter)), // เมื่อกดปุ่ม เราก็จะเรียก event โดยผ่านคำสั่ง dispatch ตามด้วย event
tooltip: 'Increment' ,
child: Icon (Icons .add),
),
);
}
);
}
หลังจากแก้ได้ตามนี้แล้ว เราก็สามารถลบ หรือคอมเม้น ตัวแปร และฟังก์ชั่นเดิมได้
int _counter = 0 ;
void _incrementCounter() {
setState(() {
_counter++;
});
}
import 'package:flutter/material.dart' ;
import 'package:bloc_test/bloc/counter.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
void main () => runApp (BlocProvider (
builder: (BuildContext context) => CounterBloc (),
child: MyApp (),
));
class MyApp extends StatelessWidget {
@override
Widget build (BuildContext context) {
return MaterialApp (
title: 'Flutter Demo' ,
theme: ThemeData (
primarySwatch: Colors .blue,
),
home: MyHomePage (title: 'Flutter Demo Home Page' ),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage ({Key key, this .title}) : super (key: key);
final String title;
@override
_MyHomePageState createState () => _MyHomePageState ();
}
class _MyHomePageState extends State <MyHomePage > {
/* int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
} */
@override
Widget build (BuildContext context) {
final counterBloc = BlocProvider .of <CounterBloc >(context); // เรียกใช้ CounterBloc
return BlocBuilder ( // เมื่อสร้าง widget ที่ต้องการนำค่าจาก bloc มาใช้ ให้นำ BlocBuilder มาคลุม
bloc: counterBloc, // ระบุ counterBloc ที่เราสร้าง
builder: (BuildContext context, CounterState state) {
final counter = (state as UpdateCounterState ).counter; // ค่านับเลขถูกเรียกจาก state โดยเราจะระบุ state ที่นำมาใช้งานด้วย เพื่อให้ complier เราสามารถเข้าใจว่ามีตัวแปรใดบ้างในคลาสของ state นี้
return Scaffold (
appBar: AppBar (
title: Text (widget.title),
),
body: Center (
child: Column (
mainAxisAlignment: MainAxisAlignment .center,
children: < Widget > [
Text (
'You have pushed the button this many times:' ,
),
Text (
/* '\$_counter', */
'\$counter ' , // เราเรียก counter มาใช้งานได้เลย
style: Theme .of (context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton (
onPressed: () => counterBloc.dispatch (IncrementCounter (counter)), // เมื่อกดปุ่ม เราก็จะเรียก event โดยผ่านคำสั่ง dispatch ตามด้วย event
tooltip: 'Increment' ,
child: Icon (Icons .add),
),
);
}
);
}
}
ตอนนี้แอปของเราก็พร้อมที่จะรันได้แล้ว
flutter run
หลังจากลองรันแล้ว เราจะพบว่าแทบไม่เห็นความแตกต่างเลย แต่ในความเป็นจริงแล้ว การที่เราเลือกใช้
flutter_bloc
จะมีความพิเศษที่ตัว state management นี้จะช่วยเราในการจัดการว่า widget ไหนจะต้องสร้างใหม่บ้าง จะเห็นผลผลอย่างมาก เมื่อหน้าตาแอปของเราประกอบไปด้วยหลายๆ widget และด้วย BLoC Pattern จะทำให้โค๊ดเราเป็นระเบียบ สามารถนำไปต่อยอดได้ง่าย
ไหนๆก็รันโค๊ดผ่านแล้ว เราลองมาเพิ่มอีกซัก event นึงโดยให้ทำหน้าที่ลดตัวเลขลงทีละ 1 และเพิ่มปุ่มลดตัวเลขด้วย
กลับไปที่
counter_event.dart
อีกรอบ
เพิ่มคลาสใหม่
DecrementCounter
ทำหน้าที่ส่ง event ลดเลข
class DecrementCounter extends CounterEvent {
final int counter;
DecrementCounter( this .counter) : super ([counter]);
@override
String toString() => 'DecrementCounter {counter : $counter }' ;
}
สุดท้ายโค๊ดจะออกมาหน้าตาตามนี้
import 'package:meta/meta.dart' ;
import 'package:equatable/equatable.dart' ;
@immutable
abstract class CounterEvent extends Equatable {
CounterEvent ([List props = const []]) : super (props);
}
class IncrementCounter extends CounterEvent {
final int counter;
IncrementCounter (this .counter) : super ([counter]);
@override
String toString () => 'IncrementCounter {counter : \$counter }' ;
}
class DecrementCounter extends CounterEvent {
final int counter;
DecrementCounter (this .counter) : super ([counter]);
@override
String toString () => 'DecrementCounter {counter : \$counter }' ;
}
เสร้จแล้วไปที่
counter_bloc.dart
เพื่อเพิ่มฟังก์ชั่นไว้รองรับ
DecrementCounter
ซึ่งทำหน้าที่ลดตัวเลขลงทีละ 1
Stream<CounterState> _mapDecrementCountertoState(DecrementCounter event) async \* {
final int counter = event.counter - 1 ;
yield UpdateCounterState(counter);
}
แล้วแก้
mapEventToState
ให้เรียกฟังก์ชั่นนี้เมื่อ event ที่ส่งมาเป็น
DecrementCounter
@override
Stream<CounterState> mapEventToState(
CounterEvent event,
) async \* {
if (event is IncrementCounter) {
yield \* _mapIncrementCountertoState(event);
} else if (event is DecrementCounter) {
yield \* _mapDecrementCountertoState(event);
}
}
import 'dart:async' ;
import 'package:bloc/bloc.dart' ;
import './counter.dart' ;
class CounterBloc extends Bloc <CounterEvent , CounterState > {
@override
CounterState get initialState {
return UpdateCounterState (0 );
}
@override
Stream <CounterState > mapEventToState (
CounterEvent event,
) async * {
if (event is IncrementCounter ) {
yield * _mapIncrementCountertoState (event);
}else if (event is DecrementCounter ) {
yield * _mapDecrementCountertoState (event);
}
}
Stream <CounterState > _mapIncrementCountertoState (IncrementCounter event) async * {
final int counter = event.counter + 1 ;
yield UpdateCounterState (counter);
}
Stream <CounterState > _mapDecrementCountertoState (DecrementCounter event) async * {
final int counter = event.counter - 1 ;
yield UpdateCounterState (counter);
}
}
logic เสร็จแล้วก็กลับไปที่
main.dart
เพื่อเพิ่มปุ่มลดกัน
เพื่อให้เราสามารถใส่ FABs ได้หลายอัน เราก็จะเอา Column/Row Widget ไปครอบ
floatingActionButton: FloatingActionButton(
onPressed: () =>
counterBloc.dispatch(IncrementCounter(counter)),
tooltip: 'Increment' ,
child: Icon(Icons.add),
),
floatingActionButton: Row(mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: () =>
counterBloc.dispatch(IncrementCounter(counter)),
tooltip: 'Increment' ,
child: Icon(Icons.add),
),
Padding(padding: EdgeInsets.only(left: 20 ),), // เพิ่มช่องว่าระหว่างปุ่ม
FloatingActionButton(
onPressed: () =>
counterBloc.dispatch(DecrementCounter(counter)), // ทำหน้าที่ส่ง DecrementCounter(counter) แทน
tooltip: 'Increment' ,
child: Icon(Icons.remove),
),
],
),
import 'package:flutter/material.dart' ;
import 'package:increment_app/bloc/counter.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
void main () => runApp (BlocProvider (
builder: (BuildContext context) => CounterBloc (),
child: MyApp (),
));
class MyApp extends StatelessWidget {
@override
Widget build (BuildContext context) {
return MaterialApp (
title: 'Flutter Demo' ,
theme: ThemeData (
primarySwatch: Colors .blue,
),
home: MyHomePage (title: 'Flutter Demo Home Page' ),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage ({Key key, this .title}) : super (key: key);
final String title;
@override
_MyHomePageState createState () => _MyHomePageState ();
}
class _MyHomePageState extends State <MyHomePage > {
@override
Widget build (BuildContext context) {
final counterBloc = BlocProvider .of <CounterBloc >(context);
return BlocBuilder (
bloc: counterBloc,
builder: (BuildContext context, CounterState state) {
final counter = (state as UpdateCounterState ).counter;
return Scaffold (
appBar: AppBar (
title: Text (widget.title),
),
body: Center (
child: Column (
mainAxisAlignment: MainAxisAlignment .center,
children: < Widget > [
Text (
'You have pushed the button this many times:' ,
),
Text (
'\$counter ' ,
style: Theme .of (context).textTheme.display1,
),
],
),
),
floatingActionButton: Row (mainAxisAlignment: MainAxisAlignment .end,
children: < Widget > [
FloatingActionButton (
onPressed: () =>
counterBloc.dispatch (IncrementCounter (counter)),
tooltip: 'Increment' ,
child: Icon (Icons .add),
),
Padding (padding: EdgeInsets .only (left: 20 ),), // เพิ่มช่องว่าระหว่างปุ่ม
FloatingActionButton (
onPressed: () =>
counterBloc.dispatch (DecrementCounter (counter)), // ทำหน้าที่ส่ง DecrementCounter(counter) แทน
tooltip: 'Decrement' ,
child: Icon (Icons .remove),
),
],
),
);
});
}
}
เมื่อลองรันอีกรอบ แอปก็จะออกมาหน้าตาดังนี้
คงต้องจบ BLoC Pattern สำหรับ Noob ไว้เท่านี้ก่อน แล้วเราจะมาต่อกันที่ การนำ Bloc มาใช้กับแอปที่มีหลายหน้า เพื่อให้ตอบโจทย์โลกความเป็นจริงด้วย
ถ้าเขียนเสร็จแล้ว จะเอาลิงค์มาลงไว้ตรงนี้
หวังว่า medium อันนี้พอจะช่วยให้หลายๆ คนเข้าใจ BLoC Pattern มากขึ้น ถ้ามีความเห็นอะไร ก็คอมเม้นลงมาข้างล่างได้เลย
แนะนำเรื่องถัดไป