web analytics

Flutter : รู้จักกับบ BLoC Pattern ใน Flutter

cove

สวัสดีครับ บล็อกนี้มาลองเล่น BLoC Pattern กันครับ เป็น Design Pattern ที่นิยมกันใน Flutter เพราะผมเห็นในกลุ่ม Flutter Developer ฝรั่งพูดถึงกันเยอะมาก

 

รู้จักกับ BLoC

BLoC ย่อมาจาก Business Logic of Component คือ Design Pattern ที่นิยมใช้ใน Flutter เพราะคนคิดคือ วิศวกรของ Google นั่นเอง โดยจุดประสงค์ก็เพื่อจัดการกับปัญหา State และ Event ต่างๆ คล้ายๆกับ Redux เพื่อเพิ่มประสิทธิภาพของแอป รวมทั้งสร้างแนวทางการพัฒนาเป็นแนวทางเดียวกัน 

 

เริ่มต้น

ลองมาเริ่มเล่น เจ้า BLoC ด้วยโปรเจคง่ายๆ
เมื่อเราสร้างโปรเจค Flutter เปล่าๆ จะได้แอปนับเลข counter

 

การทำงาน คือ กดที่ปุ่ม Floating ด้านขวาล่าง ตัวเลขก็จะเพิ่ม

a1

 

 

หลักการทำงานของแอปนี้ คือ เมื่อกดปุ่มเพิ่ม ก็จะบวกตัวแปร counter ขึ้น 1 แล้ว setState

 

ผลที่เกิดขึ้นคือ Flutter จะทำการ rebuild ใหม่ โดยการเรียก build(context)
เพื่อให้เห็นภาพผม จะ print ชื่อ Widget ออกมาตอนที่ build() ถูกเรียกว่า ได้สร้าง Widget อะไรบ้าง

 

ซึ่งจะเห็นว่า การกดปุ่มบวก 1 ครั้ง มี Widget หลายตัวถูกสร้างใหม่ ซึ่งทั้งๆที่สิ่งที่เราต้องการให้อัพเดท มีเพียงแค่ Text อันเดียวที่แสดงตัวเลขกลางจอเท่านั้น ทำให้เกิดการสร้าง Widget ที่ฟุ่มเฟือย ตัวอย่างนี้เป็นแค่แอปที่ยังไม่ซับซ้อน ซึ่งในความเป็นจริงแอปของเราต้องซับซ้อนกว่านี้แน่ ทำให้อาจเกิดการสร้าง Widget ที่ฟุ่มเฟือยจำนวนมาก

a3-1

 

Stream Widget

BLoC นำ Widget ที่ชื่อว่า Stream มาใช้ให้เป็นประโยชน์กับเรื่องนี้ Stream คือ การทำงาน ที่จะสร้างผลลัพธ์แบบเป็นลำดับ (ต่อเนื่อง) และเป็นแบบ Asynconous ด้วย (อย่างเพิ่งงงนะ)

จากปัญหาที่กล่าวไปด้านบน สามารถใช้ Stream มาจัดการได้
ก่อนอื่นสร้าง StreamController<T> โดย T คือ Type ของข้อมูลที่เก็บ ในตัวอย่างนี้เราจะเก็บค่า counter ที่เป็น  int

 

จากนั้น ตอนเลิกใช้งานก็ปิด StreamController ที่ dispose() ด้วย เพื่อเป็นการคืน memory

 

ต่อมา เพิ่ม StreamBuilder มาครอบ Text ที่แสดง counter
โดย StreamBuilder จะมี 3 parameter คือ
stream: ใส่ streamController.stream
initialData คือ ข้อมูลเริ่มต้น ในที่นี้คือ 0
Builder คือ function ที่ build Widget ออกมาก โดยใน function จะมี 2 argument คือ context กับ snapshot
ซึ่งเจ้า snapshot นี่แหละที่จะมี data มาด้วย

 

แล้วที่ method การเพิ่มค่า counter ก็เปลี่ยนการ setState มาใช้ streamController.sink.add(…) แทน
โดย Flutter เปรียบข้อมูลว่าเป็น กระแสน้ำ (Stream) ดังนั้นเวลาเราจะเพิ่มน้ำ (ข้อมูล) ก็ต้องทำผ่านอ่าง (Sink) นั่นเอง เลยเป็นที่มาของคำสั่ง sink.add()

 

ผลการทำงานคือ กดปุ่มแล้วจะเพิ่มค่า counter เหมือนเดิม

a2

 

ลองเพิ่ม print ที่ StreamBuilder เพื่อดูว่า rebuild เฉพาะ Widget ที่เราต้องการหรือเปล่า

 

ผลคือ ไม่มีการ rebuild แล้วสร้าง Widget ที่ฟุ่มเฟือยแล้ว เมื่อกดปุ่มจะมีแค่ Text ที่แสดง counter ถูกสร้างใหม่ เท่านั้น

a4-1

 

สรุปเรื่องของ Stream ในตอนนี้ คือ สร้าง StreamController เพื่อเก็บค่า เอาไปใช้ร่วมกับ StreamBuilder โดยนำไปครอบ Widget ที่เราต้องการให้อัพเดทโดยเฉพาะ ในตัวอย่างคือ Text ที่แสดง counter

2

 

การอัพเดท Widget จะไม่ใช้ setState() แต่จะใช้ผ่าน StreamController

3

 

 

BLoC Provider

ต่อมา เมื่อเรามีแอปหลายๆหน้า เราไม่สามารถนำ StreamController ไปใช้ในหน้าอื่นๆได้ มันผูกติดกับ UI มากเกินไป จึงเกิดเป็น Provider class วึ่งคอนเซ็ปมันก็คือการทำ Generic class แล้วสร้าง Custom Bloc ของเราเอง มาลองทำกัน

ก่อนอื่น เราจะแยกหน้า counter ออกจาก main.dart มาเป็นไฟล์ใหม่

1

 

ดังนั้นที่ main.dart จะได้แบบนี้

 

สร้างคลาสใหม่ชื่อ BlocProvider โดยอาจจะสร้างโฟลเดอร์แยกเพื่อความเป็นระเบียบ

2

 

ข้างใน BlocProvider เป็นการทำ Generic Class โดยจริงๆตัวมันเองคือ StatefulWidget นี่แหละ
แต่เพิ่ม Generic T ในตัวเองชื่อว่า bloc (ตั้งชื่ออะไรก็ได้) โดย bloc ที่ว่านี้จะต้อง extends BlocBase
ซึ่ง Blocbase ก็เป็น abstract class ที่นิยามว่า ต้องทำอะไรบ้าง โดยปกติที่ต้องทำแน่ๆ คือการ dispose()
เพื่อให้ StreamController.close() นั้นเอง สรุปโค้ดไฟล์นี้ copy ไปใช้ได้เลย

 

BLoC Counter

ต่อมา เราต้องสร้าง bloc ของเราเองเพื่อเก็บ StreamController ซึ่งในที่นี้คือ bloc เกี่ยวกับ counter
สร้างคลาส BlocCounter implements BlocBase

แน่นอนว่า โดยกฏของ Blocbase มันจะบังคับให้เรา เขียน dispose() ทันที

3

 

ตอนนี้ก็ให้ใส่ StreamController , counter แล้วก็ dispose() ก็คือย้ายโค้ดส่วนของ Stream ในหน้า main.dart ก่อนหน้านี้มาใส่ที่นี้นั่นเอง

 

เพิ่ม method สำหรับเพิ่มค่า counter

 

ที่ main.dart ก็เอา BlocProvider มาครอบ MyCounterPage
โดยมันมี paramter 2 ตัว ตัวแรกคือ bloc ก็ให้เราสร้าง instance ของ BlocCounter()
อีกตัวคือ child มันก็คือ widget ตัวเดิม

 

ทีนี้มาที่ หน้าของ counter บ้าง ในหน้านี้จะต้องไม่มี StreamController แล้ว เพราะเราย้ายไปที่ BlocCounter แล้ว
วิธีการที่เราจะเข้าถึง StreamController คือ ผ่านคำสั่งนี้ BlocProvider.of() 

 

สำหรับคำสั่ง .of()  นี้ เจอบ่อยใน Flutter มากๆ หากไม่รู้หลักการทำงานหรือที่มาที่ไป
แนะนำให้อ่านบทความเรื่อง BuildContext ครับ

https://benzneststudios.com/blog/flutter/summary-about-build-context-widget-state-key-in-flutter/

 

เมื่อได้ BlocCounter แล้วก็เอาไปใส่ที่ StreamBuilder นั่นเอง ส่วนปุ่มเพิ่มค่าก็เรียกผ่าน method incrementCounter() ที่เขียนเอาไว้

 

Broadcast

เพิ่มเติมอีกนิด คือเรื่อง broadcast ตามชื่อมันคือการกระจายสัญญาณ ซึ่งในที่นี้คือการดักจับ event
เช่น ทุกครั้งที่ StreamController ถูกเรียกให้ทำงาน ให้ print ค่าออกมาด้วย

ก่อนอื่น เปลี่ยนการสร้าง instance ปกติเป็น StreamController<int>.broadcast();
จากนั้นจะทำให้เราสามารถเรียกใช้คำสั่ง .listen(…) ได้

a5

 

สามารถเพิ่ม listen ได้หลายอัน แต่หากเราไม่สร้าง StreamController แบบ broadcast แล้วเพิ่ม listen ก็จะ error ทันที

 

สรุป

ตอนนี้ก็ได้ลองเล่น BLoC คร่าวๆแล้ว จัดว่าเป็นคอนเซ็ปที่ดีมากทีเดียว สรุปก็คือ BLoC ใน Flutter จะเล่นกับ StreamController ซึ่งจะไปใช้งานร่วมกับ StreamBuilder เพื่อจัดการปัญหาการ setState แล้วสร้าง Widget ฟุ่มเฟือย ต่อมาเพื่อให้การใช้งานยืดหยุ่นจึงสร้าง Generic T ขึ้นมา คือ BlocProvider โดยใช้งานร่วมกับ Bloc ที่เราสร้างขึ้นเอง แล้วย้าย StreamController มาไว้ใน Bloc ของเรา ทำให้โค้ดใน Page Widget ดู clean ขึ้นมากๆ เพราะไม่มีอะไรเลย นอกจาก รับข้อมูลมาแสดงผล แถมยังทำให้ประสิทธิภาพของแอปดีขึ้นอีกด้วย

ตัวอย่างโค้ดอยู่ที่ Github
https://github.com/benznest/bloc-pattern-flutter

 

 

Redux Pattern

Design Pattern อีกตัวที่ได้รับความนิยมมากครับ อ่านได้ที่บล็อกด้านล่างนี้นะ

Flutter : รู้จักกับ Redux Pattern ใน Flutter