web analytics

Flutter : สรุปเรื่อง BuildContext , Widget , State , Key ใน Flutter

cove

สวัสดีครับ บล็อกนี้ผมจะสรุปเรื่องพื้นฐานเกี่ยวกับ BuildContext , Widget , State , Key ใน Flutter เพราะว่าเรื่องพวกนี้เป็นเรื่องพื้นฐานมากใน Flutter อีกทั้งผมไปเจอบทความนึงที่เขียนดีมากๆเลยอยากแปล
และลองเขียนสรุปตามความเข้าใจครับ

 

ทุกอย่างเป็นเป็น Widget

ถ้าเราต้องการสร้างอะไรก็ตามที่มี layout ให้ผู้ใช้ เราต้องใช้สิ่งที่เรียกว่า Widget
ใน Flutter แทบทุกอย่างจัดว่าเป็น Widget หากเข้าไปดูคลาสต่างๆใน Flutter เช่น Text , Scaffold , Center ,  ล้วน extends มาจาก Widget
และ widget แต่ละตัวสามารถมี parent และ child และเมื่อนำ Widget มาต่อกันเป็น tree จะเรียกว่า Widget Tree

เช่น โปรเจคเริ่มต้นของ Flutter

 

ที่ build() จะถูกสร้างเป็น Widget Tree ประมาณนี้

1

 

 

BuildContext

สังเกตที่ method build จะมี argument 1 ตัว ชื่อ BuildContext
BuildContext คืออะไร?

2

BuildContext คือ object ที่เก็บ reference ของ Widget โดย 1 Widget จะมีแค่ 1 BuildContext เท่านั้น ดังนั้น BuildContext จึงเป็นตัวอธิบายความสัมพันธ์ของ Widget แต่ละตัวใน Widget Tree
ถ้า Widget หนึ่งเป็น child ของอีก Widget หนึ่ง BuildContext ของมันก็จะเป็นเชื่อมกับอีก BuildContext ในรูปแบบของ child ด้วย นี่จึงเป็นที่มาของ Widget tree ว่ามันเกิดขึ้นได้อย่างไร BuildContext เลยมีความสามารถของ pointer อยู่ในตัวมัน

 

Ancestor Widget

ใน Flutter เรียก Parent ว่า Ancestor Widget

เราสามารถใช้ BuildContext ให้วิ่งขึ้นไปหา parent ที่ต้องการที่ใกล้ที่สุดบน Tree ได้
เช่น หากอยู่ที่ Text ต้องการเรียกใช้ Scaffold สามารถใช้คำสั่ง ancestorWidgetOfExactType
วิธีคือ เรียกผ่าน BuildContext

แต่ทว่ากลับไม่สามารถใช้ context วิ่งหา child ที่ต้องการได้ เดี๋ยวจะอธิบายต่อในหัวข้อถัดไป

 

ประเภทของ Widget

Widget แบ่งเป็น 2 ประเภท คือ Stateless Widget กับ Stateful Widget
ก่อนอื่นเข้าใจคำว่า State ก่อน State คือ สถานะของ object หนึ่งๆ หรือก็คือค่าของตัวแปรใน object ในเวลานึงๆ

 

Stateless Widget

มันคือ Widget ที่รับค่ามา แล้ว build layout แสดงผลอย่างเดียว ไม่มีเปลี่ยนค่า ไม่มีการ rebuild ด้วยตัวมันเอง เลยไม่จำเป็นต้องมี State
เช่น Text , Row, Column พวกนี้รับค่ามา ก็แค่แสดงผลเท่านั้น ไม่มี interaction ใดๆ

โครงสร้างก็จะง่ายๆ คือ extends StatelessWidget
แล้วก็ override method build() ซึ่งจะ return Widget กลับไป

 

Stateful Widget

Stateful Widget ก็คือ StatelessWidget ที่อัพเกรดนั่นเอง มันสามารถ build layout แล้วยัง rebuild ได้ เมื่อมีค่าข้อมูลเปลี่ยนแปลง
เช่น TextField , CheckBox ดังนั้น Stateful Widget ก็คือ Widget ที่มี interaction เิกดขึ้น เช่น checkbox กดแล้วมีติ๊กถูก

โครงสร้างจะมีมากกว่า StatelessWidget  นิดหน่อย
โดย Stateful Widget จะมี class State เฉพาะของตัวเองเพิ่มขึ้นมา โดย method createState จะทำการสร้าง object state ของมัน
method ที่ใช้บ่อยคือ initState ที่เอาไว้ กำหนดค่าเริ่มต้น

 

การอ้างถึง ค่าตัวแปรจาก State ไปยังในคลาส StatefulWidget จะใช้ ตัวแปรที่ชื่อว่า widget
เช่น ผมรับค่า title เข้ามาใน StatefulWidget ใน State จะเรียกใช้ ให้ใช้ widget.title

3

 

และเมื่อต้องการอัพเดท widget ใช้คำสั่ง setState สิ่งที่มันทำคือ จะกำหนดค่าให้ตัวแปรใน state แล้วก็การ rebuild ใหม่เพื่ออัพเดท Widget

 

 

ความสัมพันธ์ระหว่าง State กับ BuildContext

ใน StatefulWidget คลาส State มีความสัมพันธ์กับ BuildContext โดยมันจะอยู่ด้วยกันแบบถาวร
แม้ว่า Widget จะถูกย้ายไปยังที่อื่นๆใน Widget Tree ทำให้ BuildContext อาจจะเปลี่ยนไป แต่จะยังเชื่อมกับ State อันเดิม
ทำให้ State จะไม่สามารถเข้าถึงด้วยการใช้ BuildContext ตัวอื่น ต้องใช้ BuildContext ที่มันสัมพันธ์กันเท่านั้น

 

Key ของ Widget

ใน Flutter แต่ละ Widget นั่นมี id เพื่อที่ระบุตัวของ Widget โดยใน Flutter เรียกว่า Key ซึ่งโดยปกติ key นี้ จะถูก Flutter สร้างขึ้นให้อัตโนมัติ
และเราสามารถที่จะ สร้าง Key ให้กับ Widget เองได้ เพราะที่จะระบุตัว Widget ที่เราต้องการ เช่นการ เข้าถึง State

ขอแนะนำให้ดูวิดีโอนี้ โดยวิดีโอจะอธิบายว่าเมื่อไหร่ถึงควรใช้ Key และใช้อย่างไร ใช้ Key ประเภทไหน
และเดี๋ยวผมจะสรุปให้อ่านข้างล่างอีกทีครับ

 

Key มีหลัก 2 แบบ คือ Local Key , Global Key
Local Key จะใช้สำหรับแยกแยะ Widget ต่างๆใน parent
ถ้า ตัวเลข หรือ string ก็ทำให้ unique ได้ ก็ใช้ ValueKey()
ถ้า ค่าทั่วไปไม่พอ ก็ใช้เป็น object
ถ้า object ยังมีโอกาสซ้ำกันอีก ก็ใช้ UniqueKey() ซึ่ง Flutter จะสร้าง key ให้แบบไม่มีทางซ้ำกัน
ส่วน Global Key จะใช้สำหรับทำให้เข้าถึง state ของ widget ได้จากภายนอก

4

 

ยกตัวอย่างการใช้ LocalKey เช่น กรณีที่ใช้ key เช่น  มีลิสที่มี checkbox แล้วสามารถสลับลำดับกันได้ ปัญหาคือ update state ที่อยู่ใน parent เดียวกัน และเป็น type เดียวกัน widget จะไม่ถูกอัพเดท

5

 

เพราะว่า Flutter จะเช็คจากแค่ type ของ Widget เท่านั้น พอเราย้าย Widget ใน tree ปรากฏว่า type ทั้งคู่เป็นอันเดียวกัน Flutter เลยเข้าใจว่าไม่จำเป็นต้องอัพเดท

a1

และเมื่อเรากำหนด key ให้กับ Widget
Flutter ก็จะเช็คจาก key แทน และเมื่อเปลี่ยนค่าใน State ทำให้ key ไม่ตรงกับ State ของ Widget
ทำให้เกิดการอัพเดท Widget

a2

วิธีใช้ Key คือเพิ่มใน constructor อันที่ชื่อว่า key

 

ส่วนการใช้ GlobalKey ก็เพียงประกาศตัวแปรไว้ก่อน แล้วค่อยมาใส่ key ที่  Widget

 

จากนั้นก็สามารถเข้าถึง state ได้ ผ่านตัวแปร GlobalKey

 

การเข้าถึง State จากภายนอก

มีกรณีให้พิจารณา 3 กรณี คือ
1. parent ต้องการเข้าถึง state ของ child
2. child ต้องการเข้าถึง state ของ parent
3. A กับ B ไม่ได้เป็น parent-child แต่อยู่ใน tree เดียวกัน ต้องการเข้าถึง state กันและกัน

มาดูวิธีการ ในแต่ละกรณีครับ

 

Parent เข้าถึง state ของ Child

เมื่อ Parent Widget (ใน Flutter เรียกว่า Ancestor) ต้องการเข้าถึง State ของ child
อันนี้เราสามารถใช้ GlobalKey ได้เลย ซึ่งได้อธิบายไปด้านบนแล้ว

 

Child เข้าถึง State ของ Parent

จากตอนต้นของบทความนี้ เรารู้ว่า BuildContext สามารถวิ่งขึ้นไปบน tree เพื่อหา parent หรือ ancestor ที่ต้องการได้จาก type
ดังนั้นถ้าเราสามารถวิ่งไปหา Widget ของ parent ได้ เราก็สามารถ เข้าถึง state มันได้

ก่อนอื่นเพิ่มให้ Parent Widget เก็บค่าตัวแปร ตอนสร้าง state ไว้

 

เพิ่ม method สำหรับ get ค่าของ state ใน Parent Widget

 

จากนั้นที่ child widget ก็เรียกคำสั่ง context.ancestorWidgetOfExactType(T) สิ่งที่มัน return มาคือ parent widget ที่ใกล้ที่สุด
แล้วเราก็เอาค่าของ state ของมันมาใช้งานได้

 

แต่ทว่าข้อเสียของวิธีการนี้คือ MyChildWidget ไม่รู้ว่า เมื่อไหร่ที่ตัวเองต้อง rebuild
และสามารถเข้าถึงแค่ Widget ที่เป็น Parent เท่านั้น ทำให้มันไม่สะดวก

น่าจะมีวิธีที่ครอบคลุมทั้ง 3 กรณีเลยสิ นี่จึงเป็นีที่มาของ InheritedWidget

 

InheritedWidget

InheritedWidget เป็น Widget พิเศษ ความสามารถของมันคือ ตัวมันเก็บข้อมูลกลางเอาไว้ และเมื่อนำ sub-tree มาเป็น child ของมัน จะทำให้ทุก widget ใน sub-tree สามารถเข้าถึงข้อมูลใน InheritedWidget ได้
มันเลยเป็นเหมือนตัวกลางของ Widget ใน tree ทำให้ Widget ใดๆใน sub-tree สามารถติดต่อกันได้ และเมื่อมีการอัพเดทข้อมูลใน State ของ InheritedWidget เราไม่จำเป็นต้อง rebuild ทุก Widget ใน tree แต่จะ rebuild แค่เฉพาะ Widget ที่ต้องอัพเดทข้อมูลเท่านั้น เยี่ยมเลยใช่ไหมล่ะ

เช่นมี Widget 2 อัน อยู่คนละกิ่งของ tree ไม่ได้เป็น parent หรือ child กันและกัน แต่อยากให้กดปุ่มที่ Widget A แล้วอัพเดทข้อมูลที่ Widget B โดยที่ไม่จำเป็นต้อง rebuild ใหม่ทั้ง Widget Tree

 

6

 

วิธีการสร้าง class ขึ้นมาอันนึง กำหนดให้ extends InheritedWidget

สิ่งที่ class นี้ทำ ถือครอง Widget กับ State และส่งไปให้ super class

 

 

โดยผมจะขอเรียก StatefulWidget class ที่ถูก InheritedWidget ถือครองอยู่ว่า InheritedWidgetData
โดย class InheritedWidgetData ก็เหมือน StatefulWidget  ทั่วไป
เพียงแต่ว่ามี method ที่ เรียก context.inheritFromWidgetOfExactType เพื่อให้มันไปค้นหา class InheritedWidget ใน tree
ซึ่งในที่นี้คือ MyInheritedWidget ของเรา พอเจอแล้วก็ return state ที่มันถือครองอยู่ออกมา

 

จากนั้นก็ใส่ MyInheritedWidgetData ที่ root ของ tree

 

สิ่งที่ผมจะลองทำคือ ให้ widget A มีปุ่มกด กดปุ่มแล้วให้ข้อความใน widget B เปลี่ยนแปลง โดยที่ widget C ไม่ถูก rebuild ใหม่

โดย child ไหนที่เราอยากให้เชื่อมกับ InheritedWidget ก็แค่เพิ่มคำสั่งนี้เพื่อ get state

 

ดังนั้นต้องเพิ่มที่ widget A เพราะ ต้องอัพเดท state ตัวที่ InheritedWidget ถือครองอยู่
และต้องเพิ่มที่ widget B ด้วย เพราะ ต้องเอา state ใน InheritedWidget มาแสดง
ส่วน widget C ไม่เกี่ยว ไม่ต้องทำอะไร

 

ลองรันจะได้ดังนี้

7

 

จากนั้นสังเกตที่ console log เมื่อกดปุ่ม ตัวที่จะถูก rebuild จะมีแค่ widget A และ B เท่านั้น และไม่ได้ใช้ state ภายนอกเลย แต่เป็นการใช้ state ตัวกลางของ InheritedWidget

a3

 

เมื่อเทียบกับ การทำงานแบบเดิมที่ เรา setState จากภายนอกแบบไม่ผ่าน InheritedWidget
ผมเพิ่มปุ่มสีแดงมาปุ่มนึง พอกดแล้วจะเรียก setState() เพื่ออัพเดทข้อมูล

 

ผลคือ มันจะ rebuild ใหม่ทุก Widget ใน tree

8

 

ป้องกันการเข้าถึง Inherited Widget ขณะ rebuild

เนื่องจาก method เป็น static ทำให้มันมีโอกาสที่จะเกิดการถูกแทรกขณะมี Widget นึงกำลัง rebuild อยู่
วิธีการป้องกัน คือ rebuild เมื่อจำเป็นเท่านั้น ก็แค่เพิ่ม parameter boolean มา ตัวนึง ว่าต้องการ rebuild หรือแค่เข้าถึง state เฉยๆ
ถ้าต้องการ rebuild ก็เรียก inheritFromWidgetOfExactType แต่ถ้าไม่ต้องการก็เรียก ancestorWidgetOfExactType แทน
เพราะการเรียก ancestorWidgetOfExactType จะไม่มีการ rebuild เกิดขึ้น จะวิ่งไปค้นหา parent เท่านั้น

 

ดังนั้นจากตัวอย่างด้านบน Widget A ไม่จำเป็นต้องถูก rebuild ใหม่ เพราะมันแค่ ดึงข้อมูลจาก state มาอัพเดท ไม่ได้เอามาแสดง ดังนั้น
เราก็เพิ่มเงื่อนไข ว่า rebuild = false ได้ ในตอนเรียก MyInheritedWidgetData

 

ลองรัน ทีนี้ widget ที่ถูก rebuild ใหม่ มีแค่ Widget B แล้ว

a4

 

ตัวอย่างอื่นๆ

ถ้าหากเราเขียน Flutter มาบ้างจะเห็นการใช้งาน ancestor , InheritedWidget อยู่เป็นประจำ
เช่นการแสดง SnackBar จะต้องใช้คำสั่งนี้

ซึ่งจริงๆแล้วมันก็คือ การใช้ ancestorWidgetOfExactType นั่นเอง โดยให้ buildContext วิ่งไปหา ancestor ที่สุดที่สุดบน tree แล้วเรียกคำสั่ง showSnackBarv

 

หรืออีกตัวอย่าง เวลาที่เราต้องการเปลี่ยนหน้าจอ เราจะใช้คำสั่งคือ Navigator.of(context)
ซึ่งมันก็คือ rootAncestorStateOfType และ ancestorWidgetOfExactType อีกเช่นกัน โดยคำสั่งนี้มันจะวิ่งไปหา Navigator  ซึ่งปกติเมื่อเราสร้าง MaterialWidget ก็จะมี Navigator อยู่ที่นั่น

 

สรุป

บทความนี้เป็นสรุปรวมๆ ตามความเข้าใจของผมเกี่ยวกับเรื่องพื้นฐาน เช่น State , InheritedWidget ครับ
Widget เอามาสร้างเป็น Tree โดยมี BuildContext เป็นตัว reference แต่ละ widget ใน tree เราสามารถใช้ BuildContext วิ่งขึ้นไปบน tree เพื่อหา parent ได้
Widget มี 2 แบบคือ StatefulWidget , StatelessWidget โดย StatelessWidget มีไว้แค่แสดงผลเท่านั้น ไม่สามารถ rebuild ด้วยตัวเอง แต่ StatefulWidget ทั้งแสดงผลและมี interaction ด้วย
แต่ละ Widget สามารถระบุ Key ได้ เพื่อให้มันไม่ซ้ำกัน Key มี 2 ประเภทคือ LocalKey , GlobalKey
InheritedWidget ที่เป็นคลาสพิเสษที่ทำให้ Widget ใน sub-tree ติดต่อกันได้ เป็นเหมือนตัวกลางที่เก็บ state และช่วยให้ rebuild widget ที่จำเป็นเท่านั้น ไม่ต้อง rebuild ใหม่ทั้ง Widget tree

 

จบเกม ขอบคุณที่อ่านถึงตรงนี้ครับ (:
หากมีส่วนไหนผิด สามารถติแก้ไข หรือแนะนำได้เลยนะครับ

 

โค้ดตัวอย่างเรื่อง InheritedWidget ด้านบน
https://gist.github.com/benznest/d5debe790c5e65b7467f80debb4b8373

 

Credit

https://medium.com/flutter-community/widget-state-buildcontext-inheritedwidget-898d671b7956