BuildContext class ถูกออกแบบมาให้ใช้สำหรับเป็นตัวกลางเพื่อติดต่อกับ Element class ที่เก็บข้อมูลของ widget ที่ถูกสร้างอยู่ใน widget tree โดยปกติแล้ว developer มักไม่ได้ไปยุ่งกับการสร้าง Element โดยตรง เนื่องจากเป็นส่วนที่ตัว Flutter framework เป็นตัวจัดการ ซึ่งวิธีในการจัดการเราจะไม่ได้พูดถึงเนื่องจากทางคนออกแบบ framework ไม่ได้ต้องการให้มายุ่งในส่วนนี้ หากลองดูโครงสร้างของ Widget จะเห็นว่ามีคำสั่ง createElement()
งานสร้่างแอปทั่วไปก็จะไม่เคย override เอามาใช้งานกันเลย
abstract class StatelessWidget extends Widget {
/// Initializes [key] for subclasses.
const StatelessWidget({ super.key });
/// Creates a [StatelessElement] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
StatelessElement createElement() => StatelessElement(this);
}
เมื่อลองมาดู StatelessElement จะพบว่ามัน inherite มาดังนี้
StatelessElement < ComponentElement < Element < DiagnosticableTree < Object
จะเห็นว่ามันมาจาก Element และตัว Element เองก็ implements ตัว BuildContext
abstract class Element extends DiagnosticableTree implements BuildContext {
/// Creates an element that uses the given widget as its configuration.
///
/// Typically called by an override of [Widget.createElement].
Element(Widget widget): _widget = widget {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
library: _flutterWidgetsLibrary,
className: '$Element',
object: this,
);
}
}
/// The configuration for this element.
///
/// Avoid overriding this field on [Element] subtypes to provide a more
/// specific widget type (i.e. [StatelessElement] and [StatelessWidget]).
/// Instead, cast at any call sites where the more specific type is required.
/// This avoids significant cast overhead on the getter which is accessed
/// throughout the framework internals during the build phase - and for which
/// the more specific type information is not used.
@override
Widget get widget => _widget!;
Widget? _widget;
}
หน้าที่หลักของ BuildContext คือ ใช้สำหรับจัดการเรื่องโครงสร้างของ widget ที่อยู่ใน widget tree โดย instances ของตัว BuildContext ทั้งที่ได้จากการคำสั่ง .build(BuildContext context)
หรือจาก State.context
จะมีการเปลี่ยนแปลงทุกครั้งที่มีการ rebuild ตัว widget tree และก่อนใช้งานทุกครั้งควรตรวจสอบว่ามันถูก .mounted
อยู่หรือไม่ เพื่อหลีกเลี่ยงการเกิด Exception error
build(BuildContext context)
ในคำสั่ง build(BuildContext context)
ใน StatelessWidget หรือ State จะเป็น BuildContext ของ widget tree ที่จะนำผลที่ได้จาก build ไปใส่ ดังนั้นหากต้องการดึงข้อมูลที่สนใจจาก widget tree สามารถใช้งาน context ที่ส่งมาได้
BuildContext.widget
ในตัวอย่าง จะลองพิมพ์ค่าที่ได้จากตัว context.widget
ออกมาว่ามันคือ widget ตัวไหน ผลที่ได้คือตัว MainApp
import 'package:flutter/material.dart';
import 'dart:developer';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
log(context.widget.toStringDeep()); // output → [log] MainApp
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
หากลองใส่คำสั่งนี้ในตัว Text ที่แสดงผล โดยสร้าง MyText ขึ้นมาแล้วแทนคำสั่ง Text เดิมด้วย child: MyText('Hello World!')
จะได้ผลดังนี้
class MyText extends StatefulWidget {
final String text;
const MyText(this.text, {super.key});
@override
State<MyText> createState() => _MyTextState();
}
class _MyTextState extends State<MyText> {
@override
Widget build(BuildContext context) {
log(context.widget.toStringDeep()); // output → [log] MyText
log(context.mounted.toString()); // output → [log] true
MyText myText = context.widget as MyText;
log(myText.text); // output → [log] Hello World!
return Text(widget.text);
}
}
ค่า BuildContext.size คือขนาดของ widget ที่ถูกแสดงบนจอ ลองเปลี่ยนตัว MyText เมื่อมีการแตะหรือคลิก จะแสดงขนาดใน log ดู
class _MyTextState extends State<MyText> {
@override
Widget build(BuildContext context) {
void logSize() {
log("MyText size = ${context.size}");
}
return GestureDetector(
onTap: logSize,
child: Text(widget.text),
);
}
}
แสดง log เมื่อคลิกที่ตัวข้อความ
BuildContext.size จะสามารถเข้าถึงได้หลังจากที่ตัว widget ถูกสร้างและนำเข้าไปใส่ไว้ใน widget tree แล้วเท่านั้น ไม่สามารถเรียกคำสั่งนี้ได้ใน build(BuildContext context)
หากเรียกใช้งานในขณะที่ยัง build ไม่เสร็จ จะเกิด Exception error ขึ้น
ในการสั่ง setStatus ตัว widget หากคำสั่งนี้มาจากตัว widget เองที่จะเกิดขึ้นเมื่อ widget อยู่บนหน้าจอแน่ ๆ ก็คงไม่มีปัญหาอะไร แต่หากเรียกคำสั่งนี้จากจุดที่อยู่นอก widget เช่น callback หรือเหตุการณ์แบบ asynchronous
การตรวจสอบว่าตัว widget ยังอยู่ใน widget tree หรือไม่ ให้ตรวจสอบจากคำสั่ง .mounted
ต้องมีค่าเป็น true
มิฉะนั้นจะเกิด Exception error ขึ้นได้
widget ที่เป็น child ของ InheritedWidget สามารถใช้คำสั่ง .dependOnInheritedWidgetOfExactType<T>()
เพื่ออ้างถึง InheritedWidget ดังกล่าวได้ สามารถอ่านวิธีใช้ได้ที่บทความ
ในกรณีที่ต้องการตรวจสอบว่าตัว widget นั้นอยู่ใน child ของ widget ที่มี type เป็น T หรือไม่ สามารถใช้คำสั่ง .findAncestorWidgetOfExactType<T>()
ตัวคำสั่งจะทำการค้นหาทุก Element ที่อยู่ภายใน widget tree จนกว่าจะเจอตัว type เป็น T ตามที่ระบุ และหากค้นจนครบแล้วไม่เจอจะคืนค่ากลับมาเป็น null
ตัวอย่าง เมื่อแตะหรือคลิกที่ตัวข้อความ Hello World!
จะทำการเรียก findScaffold()
และแสดงข้อความ log ออกมา
class _MyTextState extends State<MyText> {
@override
Widget build(BuildContext context) {
void findScaffold() {
Scaffold? scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
if (scaffold != null) {
log('Scaffold is ancestor of this widget');
log(scaffold.body!.toStringDeep()); // output → [log] Center(alignment: Alignment.center)
}
}
return GestureDetector(
onTap: findScaffold,
child: Text(widget.text),
);
}
}
widget tree ที่ใช้ทดสอบ
ตัว StatefulWidget จะมี State ที่เก็บสถานะของ widget หากต้องค้นหาตัว State สามารถใช้คำสั่ง .findAncestorStateOfType<T>()
โดยค่าที่ได้จะเป็น State ตัวที่อยู่ใกล้ที่สุด หากไม่พบจะคืนค่า null
โดยมีข้อควรระวังในการใช้งานดังนี้
build()
เพราะอาจมีผลทำให้ตัว widget ไม่ถูก rebuilt หากค่า State ที่ได้ถูกทำให้เปลี่ยนแปลงตัวอย่าง จะเป็นการให้ตัว MainApp แสดงข้อความที่เป็นวันที่และเวลาปัจจุบันบนหน้าจอ เมื่อแตะหรือคลิกจะทำการค้นหา _MainAppState
จากนั้นจะเรียกคำสั่ง void callFromOutside()
เพื่อให้ _MainAppState ทำการ rebuild หน้าจอใหม่
คลิกที่ข้อความเพื่อแสดงวันที่และเวลาปัจจุบัน
import 'package:flutter/material.dart';
import 'dart:developer';
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
void callFromOutside() {
// ❌ this is not good practice to rebuild widget from outside
setState(() {});
}
@override
Widget build(BuildContext context) {
var now = DateTime.now().toString();
return MaterialApp(
home: Scaffold(
body: Center(
child: MyText(now),
),
),
);
}
}
class MyText extends StatelessWidget {
final String text;
const MyText(this.text, {super.key});
@override
Widget build(BuildContext context) {
void rebuiltMainAppState() {
_MainAppState? mainAppState = context.findAncestorStateOfType<_MainAppState>();
assert(mainAppState != null);
log('try to call method in _MainAppState');
mainAppState!.callFromOutside();
}
return GestureDetector(
onTap: rebuiltMainAppState,
child: Text(text),
);
}
}
คำสั่ง .findAncestorStateOfType<T>()
เป็นคำสั่งใช้การประมวลผลมาก เพราะแอปในชีวิตจริงมีโครงสร้างที่ซับซ้อน และมี widget จำนวนมาก ดังนั้นควรใช้งานเมื่อจำเป็นจริง ๆ
ในกรณีที่ต้องการเข้าถึง widget ใด ๆ จากภายนอก แนะนำว่าให้ใช้ GlobalKey()
ผูกกับ widget ที่ต้องการ เพื่อสามารถเข้าถึงได้ทันทีไม่ต้องค้นหา
คำสั่ง .findRootAncestorStateOfType<T>()
หลักการทำงานจะทำงานเหมือน .findAncestorStateOfType<T>()
คือหาตัว State ตาม type ที่ระบุ แต่จะหาตัวที่อยู่ไกลที่สุดแทน
ในการใช้งาน widget หลายตัว มักมีการเอา BuildContext ไปใช้เพื่อดึง widget นั้นออกมา ตัวอย่างเช่น
Theme.of(context)
MediaQuery.of(context)
Navigator.of(context)
DrawerController.of(context)
Material.of(context)
MaterialLocalizations.of(context)
Scaffold.of(context)
DefaultTextStyle.of(context)
Form.of(context)
Overlay.of(context)
PageStorage.of(context)
ScrollController.of(context)
Scrollable.of(context)
View.of(context)