Rebuilds & Keys
React ใช้ virtual DOM diffing algorithm เพื่อตัดสินว่า components ใดควรอัปเดต Flutter ใช้การ reconcile element tree แบบคล้ายกัน แต่มีความแตกต่างสำคัญในวิธีที่คุณควบคุมการ reconcile — หลักๆ ผ่าน const constructors และ Keys
การเข้าใจว่า Flutter rebuild เมื่อไหร่เป็นสิ่งสำคัญสำหรับการเขียน apps ที่มีประสิทธิภาพ mental model ใกล้เคียงกับ React แต่ tools แตกต่างกัน
เมื่อไหร่ Flutter rebuild?
หัวข้อที่มีชื่อว่า “เมื่อไหร่ Flutter rebuild?”build() ของ widget ถูกเรียกเมื่อ:
setState()ถูกเรียกบนStateobject ของตัวเอง- Parent ของตัวเอง rebuild (และส่ง widget configuration ใหม่)
InheritedWidgetที่ widget นั้น depend on เปลี่ยน (เช่นTheme,MediaQuery)
ใน React trigger เทียบเท่าคือ: state setter call, parent re-render, หรือ context value change
const widgets — zero-cost optimization
หัวข้อที่มีชื่อว่า “const widgets — zero-cost optimization”ใน React คุณเอื้อมหา React.memo, useMemo, และ useCallback เพื่อป้องกัน re-renders ที่ไม่จำเป็น ใน Flutter คุณเอื้อมหา const — และมีพลังมากกว่าเพราะทำงานที่ระดับภาษา/compiler
const widget เป็น compile-time constant Flutter element reconciliation ตรวจสอบ identity: ถ้า widget reference ใน build output ใหม่ เหมือนกัน กับของก่อนหน้า (same object ใน memory) element subtree ทั้งหมดถูกข้าม const รับประกัน identity นี้
// React: opt-out จาก re-render ด้วย React.memoconst Header = React.memo(function Header({ title }) { return <h1>{title}</h1>;});
// React: stable reference ด้วย useMemofunction Page() { const icon = useMemo(() => <Icon name="star" />, []); return <div>{icon}<Content /></div>;}// Flutter: const widget ไม่ถูก rebuild ถ้า parent rebuildclass Header extends StatelessWidget { final String title; const Header({super.key, required this.title});
@override Widget build(BuildContext context) { return Text(title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)); }}
class Page extends StatelessWidget { const Page({super.key});
@override Widget build(BuildContext context) { return Column( children: [ // Subtree นี้เป็น compile-time constant — Flutter จะ // ไม่เรียก build() ของตัวเองอีก แม้ Page จะ rebuild const SizedBox(height: 16), const Icon(Icons.star, size: 32), const Header(title: 'My App'), // const ด้วย — free skip ], ); }}Keys — identity ข้ามการ rebuild
หัวข้อที่มีชื่อว่า “Keys — identity ข้ามการ rebuild”key prop ของ React บอก reconciler ว่า: “element นี้มี identity นี้ — match กับ element เดิมใน render ก่อนหน้า แม้ position ใน list จะเปลี่ยน” Key ของ Flutter ทำหน้าที่เหมือนกันทุกประการ
ถ้าไม่มี Key Flutter จะ match widgets ตาม type และ position ถ้าคุณ reorder list ของ stateful items Flutter อาจ match State object ผิดกับ widget ผิด — เหมือน bug ใน React ที่ไม่มี list keys
// React — key บน list items ป้องกัน state mixingfunction TodoList({ todos }) { return ( <ul> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} /> // key ด้วย stable id ))} </ul> );}// Flutter — Key บน list itemsclass TodoList extends StatelessWidget { final List<Todo> todos; const TodoList({super.key, required this.todos});
@override Widget build(BuildContext context) { return ListView( children: todos .map((todo) => TodoItem( key: ValueKey(todo.id), // stable id key todo: todo, )) .toList(), ); }}ตัวอย่างที่รันได้
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้”import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange)),
home: const KeyDemo(),
);
}
}
class KeyDemo extends StatefulWidget {
const KeyDemo({super.key});
@override
State<KeyDemo> createState() => _KeyDemoState();
}
class _KeyDemoState extends State<KeyDemo> {
List<String> _items = ['Apple', 'Banana', 'Cherry'];
bool _useKeys = false;
void _shuffle() => setState(() => _items = [..._items]..shuffle());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Keys Demo')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Text('Use ValueKey:'),
const SizedBox(width: 8),
Switch(
value: _useKeys,
onChanged: (v) => setState(() => _useKeys = v),
),
const Spacer(),
FilledButton(
onPressed: _shuffle,
child: const Text('Shuffle'),
),
],
),
),
Expanded(
child: ListView(
children: _items.map((item) {
// ด้วย keys: Flutter match ColorBox กับ item เดิมหลัง shuffle
// ไม่มี keys: Flutter match ตาม position — สีจะ "กระโดด" ไปผิด items
return ColorBox(
key: _useKeys ? ValueKey(item) : null,
label: item,
);
}).toList(),
),
),
],
),
);
}
}
// Stateful widget ที่เก็บ random color เป็น state ของตัวเอง
// ไม่มี Key การ shuffle จะ reorder labels แต่ colors ยังอยู่ใน
// position เดิม — classic key-less reconciliation bug
class ColorBox extends StatefulWidget {
final String label;
const ColorBox({super.key, required this.label});
@override
State<ColorBox> createState() => _ColorBoxState();
}
class _ColorBoxState extends State<ColorBox> {
late final Color _color;
@override
void initState() {
super.initState();
// สีจาก label hashCode — stable ต่อ State instance
final hue = (widget.label.hashCode % 360).abs().toDouble();
_color = HSLColor.fromAHSL(1, hue, 0.6, 0.7).toColor();
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _color,
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.label,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18),
),
);
}
}