InheritedWidget เป็น widget อีกประเภทที่ออกแบบมาเพื่อให้ widget tree ที่อยู่ใน child ของมัน ไม่ว่าจะลึกแค่ไหน ให้สามารถแชร์ข้อมูลจากตัว InheritedWidget ที่อยู่ด้านบนได้ผ่าน BuildContext ที่ส่งผ่านไปในคำสั่ง build(BuildContext context)
ของ widget ที่อยู่ใน child
ตัว InheritedWidget จะตรวจสอบทุกครั้งที่มีการ rebuild ตัวมันเอง หากข้อมูลที่ผูกติดกับตัวมันมีการเปลี่ยนแปลง มันจะทำการ rebuild ตัว widget ที่อยู่ใน child แต่หากข้อมูลไม่เปลี่ยนแปลงมันก็จะไม่ทำอะไร
การผูก data A B C ไปยัง InheritedWidget โดย widget ที่อยู่ลึกลงไปสามารถเข้าถึงข้อมูลได้
InheritedWidget ถูกกำหนดมาเป็น immutable ไม่สามารถเปลี่ยนแปลงได้หลังจากที่สร้าง instance ดังนั้นสมาชิกข้อมูลที่จะใช้สำหรับผูกนั้น จะต้องประกาศเป็น final
โครงสร้างของ InheritedWidget class มีดังนี้
abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ super.key, required super.child });
@override
InheritedElement createElement() => InheritedElement(this);
@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
สิ่งที่จำเป็นในการสร้าง InheritedWidget มีดังนี้
updateShouldNotify()
ตัวอย่างการสร้าง InheritedWidget ที่ผูกข้อมูล int
ชื่อ a
และ b
class ExampleA extends InheritedWidget {
final int a, b; // "a" and "b" can read from any widgets in child
const ExampleA({ required this.a, required this.b, required super.child, super.key });
@override
bool updateShouldNotify(covariant ExampleA oldWidget) {
// Flutter framework call every time when rebuild this InheritedWidget
// Compares the results of old and new data, returning true if there are changes.
return oldWidget.a != a || oldWidget.b != b;
}
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
int dataA, dataB, dataC;
@override
Widget build(BuildContext context) {
Widget renderWidgetTree = ...; // widgets to be rendered on the screen
// attach this.dataA and this.dataB to InheritedWidget
return ExampleA(a: dataA, b: dataB, child: renderWidgetTree);
}
}
widget ที่อยู่ใน child ของ InheritedWidget สามารถอ่านข้อมูลที่แชร์มาจาก InheritedWidget ผ่าน BuildContext ด้วยคำสั่ง dependOnInheritedWidgetOfExactType<T extends InheritedWidget>()
ผลที่ได้กลับมา หากตัว widget อยู่ใน child ของ InheritedWidget ตัวคำสั่งจะคืนค่าตัว InheritedWidget ที่อยู่ใกล้ที่สุดมาให้ แต่หากไม่พบจะคืนค่า null
คำสั่ง dependOnInheritedWidgetOfExactType เมื่อถูกเรียก ตัว framework จะทำการลงทะเบียน widget ที่มีการเรียกใช้ และเมื่อตัว InheritedWidget มีการเปลี่ยนแปลงค่าข้อมูลที่ถูกผูกอยู่ ตัว widget ที่เรียก dependOnInheritedWidgetOfExactType จะถูก rebuilt ดั้งนั้นหากไม่ต้องการให้ตัว widget ที่อยู่ใน child ของ InheritedWidget ถูก rebuilt สามารถใช้คำสั่ง getInheritedWidgetOfExactType<T extends InheritedWidget>()
แทน
ตัว widget ที่เรียกใช้ dependOnInheritedWidgetOfExactType เมื่อถูก rebuilt จะถูกเรียกคำสั่ง State.didChangeDependencies
ด้วย สามารถเขียน code เพิ่มเติมเพื่อจัดการเหตุการณ์ดังกล่าวได้
return Text(dataA.toString());
}
## การประยุกต์ใช้งาน
ตัว InheritedWidget ออกมาแบบมาเพื่อช่วยตรวจสอบว่าข้อมูลที่ผูกไว้มีการเปลี่ยนแปลงหรือไม่ ถ้าไม่มีการเปลี่ยนแปลง (จากการถูกเรียกคำสั่ง `updateShouldNotify()`) มันจะไม่ทำอะไร ตัว widget ที่อยู่ใน child ก็จะไม่มีการเปลี่ยนแปลง แต่หากข้อมูลที่ผูกเอาไว้เปลี่ยน มันจะทำการ rebuild ตัว widget ที่ตัว child อัตโนมัติ
### สร้าง InheritedWidget แยกการทำงานของแต่ละ widget
วิธีนี้เป็นการใช้งานตรงไปตรงมา คือ เขียน class ใหม่สำหรับงานใดงานหนึ่ง เช่น มี 2 widget ที่จะใช้แสดงบนหน้าจอและเปลี่ยนแปลงค่าเมื่อข้อมูลถูกเปลี่่ยน
<div class="image"><img src="images/20241113/20241113_0200_demo1.png" class="u-max-full-width" /><div><p>ตัวอย่าง widget 2 ตัวที่แชร์ข้อมูลจาก widget หลัก และปุ่มสำหรับเปลี่ยนข้อมูล</p>
</div></div>
- ข้อมูลที่ต้องเปลี่ยนคือ Text1 Text2 Text3
- ปุ่ม setState1 เปลี่ยนค่าของ Text1 Text2
- ปุ่ม setState2 เปลี่ยนค่า Text3
- `_Widget1` `_Widget2` เป็น InheritedWidget
- `_W1` `_W2` เป็น StatefulWidget ที่จะผูกไว้กับ child ของ `_Widget1` `_Widget2`
- ใน InheritedWidget จะใส่ debugPrint เพื่อแสดงค่าผลลัพธ์การตรวจสอบการเปลี่ยนแปลงของข้อมูลใน `updateShouldNotify()`
- ใน StatefulWidget จะใส่ debugPrint เพื่อแจ้งว่าตัว widget ถูกเรียกคำสั่ง `build()`
code ในส่วนของ `_Widget1` และ `_Widget2`
```dart
class _Widget1 extends InheritedWidget {
final String text1, text2;
const _Widget1(
{required this.text1, required this.text2, required super.child, super.key});
@override
bool updateShouldNotify(covariant _Widget1 oldWidget) {
bool r = oldWidget.text1 != text1 || oldWidget.text2 != text2;
debugPrint('_Widget1 $r');
return r;
}
}
class _Widget2 extends InheritedWidget {
final String text3;
const _Widget2({required this.text3, required super.child, super.key});
@override
bool updateShouldNotify(covariant _Widget2 oldWidget) {
bool r = oldWidget.text3 != text3;
debugPrint('_Widget2 $r');
return r;
}
}
code ในส่วนของ _W1
และ _W2
ที่จะดึงข้อมูลจาก _Widget1
และ _Widget2
มาแสดง
class _W1 extends StatefulWidget {
const _W1({super.key});
@override
State<StatefulWidget> createState() => _StateW1();
}
class _StateW1 extends State {
@override
Widget build(BuildContext context) {
debugPrint('_StateW1.build()');
String text1, text2;
_Widget1? widget1 = context.dependOnInheritedWidgetOfExactType<_Widget1>();
assert(widget1 != null);
text1 = widget1!.text1;
text2 = widget1!.text2;
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.red)),
child: Table(defaultColumnWidth: const IntrinsicColumnWidth(), children: [
TableRow(children: [const Text('Lable1:'), Text(text1)]),
TableRow(children: [const Text('Label2:'), Text(text2)])
]));
}
}
class _W2 extends StatefulWidget {
const _W2({super.key});
@override
State<StatefulWidget> createState() => _StateW2();
}
class _StateW2 extends State {
@override
Widget build(BuildContext context) {
debugPrint('_StateW2.build()');
String text3;
_Widget2? widget2 = context.dependOnInheritedWidgetOfExactType<_Widget2>();
assert(widget2 != null);
text3 = widget2!.text3;
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.blue)),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [TableRow(children: [const Text('Lable3:'), Text(text3)])]
));
}
}
code ในส่วนของ MainApp ที่จะรวมทุกอย่างเข้าด้วยกัน
String text1 = 'Text1', text2 = 'Text2', text3 = 'Text3';
ตัวแปรที่จะใช้สำหรับแสดงข้อมูลใน _W1
และ _W2
โดยมี _Widget1
และ _Widget2
ที่เป็น InheritedWidget สำหรับผูกค่าดังกล่าวเอาไว้initState()
จะทำการประกาศปุ่มสำหรับกด setState1 และ setState2 ในตัวแปร late Widget button1, button2;
build()
จะเอาทุกส่วนมาประกอบกันเพื่อสร้างผลลัพธ์ที่ต้องการแสดงบนหน้าจอclass MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<StatefulWidget> createState() => _StateMainApp();
}
class _StateMainApp extends State {
String text1 = 'Text1', text2 = 'Text2', text3 = 'Text3';
late Widget button1, button2;
@override
void initState() {
super.initState();
button1 = OutlinedButton(
key: const ValueKey('button1'),
onPressed: () => setState(() {
debugPrint(DateTime.timestamp().toString());
text1 = DateTime.now().millisecond.toString();
text2 = DateTime.now().second.toString();
}),
child: const Text('setState1'));
button2 = OutlinedButton(
key: const ValueKey('button2'),
onPressed: () => setState(() {
debugPrint(DateTime.timestamp().toString());
text3 = DateTime.now().microsecond.toString();
}),
child: const Text('setState2'));
}
@override
Widget build(BuildContext context) {
Widget widget1, widget2, outputBody;
widget1 = _Widget1(text1: text1, text2: text2, child: const _W1());
widget2 = _Widget2(text3: text3, child: const _W2());
outputBody = Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [widget1, widget2]),
Row(children: [button1, button2]),
],
);
return MaterialApp(home: Scaffold(body: outputBody));
}
}
เมื่อทำการ debug จะพบว่าโปรแกรมแสดงออกมาหน้าจอดังนี้
เมื่อเริ่ม debug โปรแกรม
เมื่อลองกดปุ่ม setState1 จะทำคำสั่งเพื่อให้ text1
และ text2
มีค่าเปลี่ยนแปลง โดยใช้เลขของเวลาขณะที่กดปุ่ม
setState(() {
debugPrint(DateTime.timestamp().toString());
text1 = DateTime.now().millisecond.toString();
text2 = DateTime.now().second.toString();
})
เมื่อกดปุ่ม setState1
ผลการทำงานจะเห็นว่าตัว _StateW1
นั้นถูกเรียก build()
เพียงตัวเดียว เนื่องจากค่าที่ได้จากการเปรียบเทียบใน updateShouldNotify()
ของ _Widget1
เป็น true
เมื่อลองกดปุ่ม setState2 จะทำคำสั่งเพื่อให้ text3
มีค่าเปลี่ยนแปลง โดยใช้เลขของเวลาขณะที่กดปุ่ม
setState(() {
debugPrint(DateTime.timestamp().toString());
text3 = DateTime.now().microsecond.toString();
})
เมื่อกดปุ่ม setState2
ผลการทำงานจะเห็นว่าตัว _StateW2
นั้นถูกเรียก build()
เพียงตัวเดียว เนื่องจากค่าที่ได้จากการเปรียบเทียบใน updateShouldNotify()
ของ _Widget2
เป็น true
จากผลการทำงานจะเห็นได้ว่า InheritedWidget ช่วยดูแลในส่วนของการ rebuild ตัว widget ไม่ต้องเขียนคำสั่ง setState()
กับตัว _W1
หรือ _W2
โดยตรง แค่ rebuild ตัว InheritedWidget ถ้าข้อมูลที่ถูกผูกไว้เปลี่ยนแปลง ตัว InheritedWidget ก็จะไปทำการสั่งให้ rebuild ตัว widget ที่อยู่ใน child ให้เอง ส่วนตัวไหนที่ไม่เปลี่ยนก็จะไม่ต้อง rebuild ให้เสียเวลา
ในบางครั้งหากงานมันไม่ซับซ้อนมาก และมี StatefulWidget หลายตัวที่ต้องการใช้ InheritedWidget ก็เขียนแบบ InheritedWidget ตัวเดียว ใช้งานกับ StatefulWidget ได้หลายตัวก็สามารถทำได้ ในการออกแบบก็แล้วแต่ความขี้เกียจของคนออกแบบว่าจะเอาแบบเขียนน้อยหรือเขียนมาก แต่แน่ ๆ คือลดการประกาศ class ได้
code ใหม่จะใส่เลข module เข้าไปเพื่อแยกว่าจะใช้กับตัวไหน _W1
หรือ _W2
ตัว logic ใน updateShouldNotify()
ตามเลข module ส่วนตัว text1 text2 text3 จะกำหนดให้มี default value เพื่อจะได้ผูกเฉพาะตัวแปรที่ใช้งานเท่านั้น
class _WidgetX extends InheritedWidget {
final int module; // 1 for _W1 , 2 for _W2
final String text1, text2, text3;
const _WidgetX(
{required this.module,
this.text1 = 'no data!!',
this.text2 = 'no data!!',
this.text3 = 'no data!!',
required super.child,
super.key});
@override
bool updateShouldNotify(covariant _WidgetX oldWidget) {
// compare by module number
if (module == 1) {
return oldWidget.text1 != text1 || oldWidget.text2 != text2;
} else {
return oldWidget.text3 != text3;
}
}
}
ในส่วนของ _W1
และ _W2
ก็อ้าง InheritedWidget ชื่อ _WidgetX
แทนของเดิม
class _W1 extends StatefulWidget {
const _W1({super.key});
@override
State<StatefulWidget> createState() => _StateW1();
}
class _StateW1 extends State {
@override
Widget build(BuildContext context) {
debugPrint('_StateW1.build()');
String text1, text2;
_WidgetX? widget1 = context.dependOnInheritedWidgetOfExactType<_WidgetX>();
assert(widget1 != null);
text1 = widget1!.text1;
text2 = widget1.text2;
return ...; //...same as old code...
}
}
class _W2 extends StatefulWidget {
const _W2({super.key});
@override
State<StatefulWidget> createState() => _StateW2();
}
class _StateW2 extends State {
@override
Widget build(BuildContext context) {
debugPrint('_StateW2.build()');
String text3;
_WidgetX? widget1 = context.dependOnInheritedWidgetOfExactType<_WidgetX>();
assert(widget1 != null);
text3 = widget1!.text3;
return ...; //...same as old code...
}
}
code ในตอนที่สร้าง ที่เพิ่ม module เข้าไป
@override
Widget build(BuildContext context) {
Widget widget1, widget2, outputBody;
widget1 = _WidgetX(module: 1, text1: text1, text2: text2, child: const _W1());
widget2 = _WidgetX(module: 2, text3: text3, child: const _W2());
ได้ผลการทำงานเหมือนกัน
.of()
และ .maybeOf()
จากตัวอย่างที่ผ่านมา หาก widget ที่อยู่ใน child ต้องการเข้าถึง InheritedWidget ที่ถูกผูกไว้ ต้องใช้คำสั่ง .dependOnInheritedWidgetOfExactType<T>()
ดูไม่เป็นมิตรต่อการอ่าน code เท่าไหร่ ถ้าดูคำสั่งใน class ของ Flutter framework ที่ inheritance ตัว InheritedWidget มันจะมีคำสั่งแบบ static ชื่อ .of()
ที่ทำงานเหมือนกัน แต่สั้นกว่า เช่น RadioTheme.of()
ในคำสั่ง .of()
จาก RadioTheme.of()
จะมีพฤติกรรมว่าหากไม่พบตัว Theme ที่อยู่ใน BuildContext มันจะคืนค่ากลับมาเป็น radioTheme แทน จะเห็นว่าเมื่อมันไม่มีข้อมูลให้ดึงก็จะส่งค่ากลับมาเป็นค่าเริ่มต้นสักอย่างที่ออกแบบไว้
ในกรณีที่ตัว widget อาจไม่ได้ผูกติดกับตัว InheritedWidget และต้องการคืนค่าเป็น null
ให้เพิ่มคำสั่ง .maybeOf()
.of()
คืนค่าเป็น InheritedWidget ตัวที่อยู่ใกล้ที่สุดที่พบ.maybeOf()
คืนค่าเป็น InheritedWidget ที่เจอ หรือคืนค่า null
หากไม่พบcode ของ InheritedWidget ที่เพิ่ม .of()
และ .maybeOf()
class _WidgetX extends InheritedWidget {
// -- old code --
// add 2 static methods
static _WidgetX of(BuildContext context) {
_WidgetX? result = maybeOf(context);
assert(result != null); // in debug mode if (result == null) → throw error
return result!;
}
static _WidgetX? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_WidgetX>();
}
}
ใน widget ที่เรียกข้อมูลที่ผูกไว้กับ InheritedWidget เขียนใหม่ได้ดังนี้
String text1, text2;
// _WidgetX? widget1 = context.dependOnInheritedWidgetOfExactType<_WidgetX>();
_WidgetX widget1 = _WidgetX.of(context);
text1 = widget1.text1;
text2 = widget1.text2;
แต่สำหรับ InheritedWidget ที่สร้างขึ้นมาจากตัวอย่างข้างบน คำสั่ง .of()
หากไม่พบ InheritedWidget ที่ระบุ การทำงานก็จะล้มเหลวจากคำสั่ง assert(result != null);
เพื่อป้องกันการใช้งานไปผูกกับ InheritedWidget ผิดตัวเท่านั้น และมีผลเฉพาะตอน debug หากลืม test แล้วไปใช้งานก็อาจจะเกิด Exception ตามมาได้ ดังนั้นหากต้องการจัดการกับในกรณีที่ตัว widget ไม่ได้ไปผูกไว้กับ InheritedWidget ที่ถูกต้องและได้ค่า null
กลับมา ควรเรียกคำสั่ง .maybeOf()
จะตรงกับจุดประสงค์มากกว่า และทำให้กลับมาอ่าน code ภายหลังเข้าใจมากกว่าด้วย
ในกรณีที่ต้องการให้ตัว widget อ่านข้อมูลอย่างเดียว ไม่ต้องการให้ rebuild เมื่อข้อมูลเปลี่ยน มีคนอื่นแนะนำว่าให้เพิ่ม rebuild เข้าไปเป็น optional parameter อีกตัว
class _WidgetX extends InheritedWidget {
// -- old code --
// add 2 static methods
static _WidgetX of(BuildContext context, {bool rebuild = true}) {
_WidgetX? result = maybeOf(context, rebuild: rebuild);
assert(result != null); // in debug mode if (result == null) → throw error
return result!;
}
static _WidgetX? maybeOf(BuildContext context, {bool rebuild = true}) {
if(rebuild) {
return context.dependOnInheritedWidgetOfExactType<_WidgetX>();
}
else {
return context.getInheritedWidgetOfExactType<_WidgetX>();
}
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<StatefulWidget> createState() => _StateMainApp();
}
class _StateMainApp extends State {
String text1 = 'Text1', text2 = 'Text2', text3 = 'Text3';
late Widget button1, button2;
@override
void initState() {
super.initState();
button1 = OutlinedButton(
key: const ValueKey('button1'),
onPressed: () => setState(() {
debugPrint(DateTime.timestamp().toString());
text1 = DateTime.now().millisecond.toString();
text2 = DateTime.now().second.toString();
}),
child: const Text('setState1'));
button2 = OutlinedButton(
key: const ValueKey('button2'),
onPressed: () => setState(() {
debugPrint(DateTime.timestamp().toString());
text3 = DateTime.now().microsecond.toString();
}),
child: const Text('setState2'));
}
@override
Widget build(BuildContext context) {
Widget widget1, widget2, outputBody;
widget1 = _WidgetX(module: 1, text1: text1, text2: text2, child: const _W1());
widget2 = _WidgetX(module: 2, text3: text3, child: const _W2());
outputBody = Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [widget1, widget2]),
Row(children: [button1, button2]),
]
);
return MaterialApp(home: Scaffold(body: outputBody));
}
}
class _WidgetX extends InheritedWidget {
final int module;
final String text1, text2, text3;
const _WidgetX(
{required this.module,
this.text1 = 'no data!!',
this.text2 = 'no data!!',
this.text3 = 'no data!!',
required super.child,
super.key});
@override
bool updateShouldNotify(covariant _WidgetX oldWidget) {
if (module == 1) {
return oldWidget.text1 != text1 || oldWidget.text2 != text2;
} else {
return oldWidget.text3 != text3;
}
}
static _WidgetX? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_WidgetX>();
}
static _WidgetX of(BuildContext context) {
_WidgetX? result = maybeOf(context);
assert(result != null);
return result!;
}
}
class _W1 extends StatefulWidget {
const _W1({super.key});
@override
State<StatefulWidget> createState() => _StateW1();
}
class _StateW1 extends State {
@override
Widget build(BuildContext context) {
debugPrint('_StateW1.build()');
String text1, text2;
_WidgetX widget1 = _WidgetX.of(context);
text1 = widget1.text1;
text2 = widget1.text2;
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.red)),
child: Table(defaultColumnWidth: IntrinsicColumnWidth(), children: [
TableRow(children: [Text('Lable1:'), Text(text1)]),
TableRow(children: [Text('Label2:'), Text(text2)])
]));
}
}
class _W2 extends StatefulWidget {
const _W2({super.key});
@override
State<StatefulWidget> createState() => _StateW2();
}
class _StateW2 extends State {
@override
Widget build(BuildContext context) {
debugPrint('_StateW2.build()');
String text3;
_WidgetX widget1 = _WidgetX.of(context);
text3 = widget1.text3;
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.blue)),
child: Table(defaultColumnWidth: IntrinsicColumnWidth(), children: [
TableRow(children: [Text('Lable3:'), Text(text3)]),
]));
}
}