الأشجار الثلاثة: Widget وElement وRenderObject
افهم كيفية مواءمة Flutter لأشجار widget وelement وrender أثناء عمليات إعادة البناء
الأشجار الثلاثة: Widget وElement وRenderObject درس مجاني في Flutter Mobile Development على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Flutter Mobile Development، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Flutter Mobile Development 4 دروس في المجموع.
بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.
Why Three Trees?
When you write Flutter UI, you describe widgets. But the framework actually maintains three parallel trees that work together every frame:
- Widget tree — immutable configuration objects you build in
build(). - Element tree — the mutable, long-lived bridge that holds state and tracks position in the tree.
- RenderObject tree — the objects that actually do layout, painting, and hit-testing.
Understanding this split is the key to reasoning about rebuild cost, why const widgets are cheap, and why a misplaced Key can corrupt state.
Widgets Are Immutable Blueprints
A Widget is just a lightweight, immutable description of a piece of UI. It holds no mutable state and is cheap to create and discard. Calling build() throws away the old widget objects and produces fresh ones every time.
Because widgets are throwaway, comparing two widgets is fast. Flutter uses that comparison to decide whether the heavier element and render objects can be reused rather than rebuilt.
class Greeting extends StatelessWidget {
const Greeting({super.key, required this.name});
final String name;
@override
Widget build(BuildContext context) {
// A brand-new Text widget is created on every rebuild.
return Text('Hello, $name');
}
}Elements Are the Living Tree
An Element is created by a widget via createElement(). Unlike widgets, elements are mutable and long-lived. The element tree is the actual runtime structure Flutter walks every frame.
Each element holds a reference to its current widget. When a rebuild happens, the element is handed a new widget and decides whether to keep itself (update in place) or be replaced. This is where reconciliation happens.
StatelessElement— backs aStatelessWidget.StatefulElement— owns theStateobject, which is why state survives rebuilds.
State Lives in the Element, Not the Widget
This is the crux of why Flutter's design works. The State object is held by the StatefulElement, which persists across rebuilds. The StatefulWidget itself is replaced on every parent rebuild — but the element (and its state) stays.
That is why your counter does not reset when the parent rebuilds: the widget is new, but the element and its State are the same instance.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0; // Survives parent rebuilds because the element persists.
void _increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _increment,
child: Text('Count: $_count'),
);
}
}RenderObjects Do the Heavy Lifting
Not every widget creates a render object. Only RenderObjectWidgets (like Padding, Opacity, Text's internals) produce entries in the render tree. Composition widgets like StatelessWidget and StatefulWidget only orchestrate other widgets — they have no render object.
RenderObjects implement performLayout(), paint(), and hit-testing. They are the most expensive objects, so Flutter works hard to update them in place instead of recreating them.
- Widget tree: deep (lots of composition widgets).
- Element tree: same depth (one element per widget).
- Render tree: shallower (only render-object widgets appear).
Reconciliation: updateChild
The reconciliation algorithm lives in Element.updateChild(oldElement, newWidget). For each child position, Flutter decides among four outcomes based on the new widget and the old element:
- Update in place — if the widgets can match, reuse the element and render object.
- Replace — if they can't match, deactivate the old subtree and inflate a new one.
- Insert — new widget, no old element.
- Remove — old element, no new widget.
The cheap path (update in place) is what keeps Flutter fast.
The Matching Rule: canUpdate
Whether an element can be reused is decided by the static method Widget.canUpdate(oldWidget, newWidget). The rule is simple but critical:
Two widgets match if and only if their runtimeType and key are equal.
If they match, the existing element keeps its position and render object and just swaps in the new widget's configuration. If they don't match, the old element is discarded and a fresh subtree is inflated — losing any associated State.
// This is the actual decision rule used during reconciliation.
bool canUpdate(Object oldType, Object? oldKey, Object newType, Object? newKey) {
return oldType == newType && oldKey == newKey;
}
void main() {
// Same type, same (null) key -> reuse element.
print(canUpdate('Text', null, 'Text', null)); // true
// Different type -> rebuild subtree, state is lost.
print(canUpdate('Text', null, 'Container', null)); // false
// Same type, different keys -> NOT a match.
print(canUpdate('Text', 'a', 'Text', 'b')); // false
}Why const Widgets Skip Rebuilds
When you mark a widget const, the same canonical instance is reused across builds. During reconciliation, updateChild sees that identical(oldWidget, newWidget) is true and can short-circuit the entire subtree — no element update, no render-object work.
This is why adding const to leaf widgets is one of the cheapest performance wins: identical widget references let Flutter prune whole branches from reconciliation.
class Header extends StatelessWidget {
const Header({super.key});
@override
Widget build(BuildContext context) {
// The const child is identical across rebuilds, so its element
// subtree is skipped entirely during reconciliation.
return const Padding(
padding: EdgeInsets.all(16),
child: Text('Settings'),
);
}
}Keys Disambiguate Same-Type Siblings
When a list contains multiple siblings of the same type, Flutter matches them by position by default. If you reorder them, the elements (and their state) stay glued to the old position — a classic source of bugs in stateful lists.
A Key changes the matching identity. With keys, reconciliation matches by (runtimeType, key) instead of position, so elements and their State follow the widget to its new slot. Use ValueKey for stable data identity.
class ReorderableTiles extends StatelessWidget {
const ReorderableTiles({super.key, required this.items});
final List<String> items;
@override
Widget build(BuildContext context) {
return Column(
children: [
for (final id in items)
// ValueKey lets each element's State follow its data on reorder.
TodoTile(key: ValueKey(id), id: id),
],
);
}
}GlobalKey: Moving an Element Across the Tree
A LocalKey (like ValueKey) only disambiguates siblings under the same parent. A GlobalKey is unique across the entire app and lets an element — with its render object and state intact — be moved to a completely different parent without being rebuilt.
GlobalKeys are powerful but costly: they force the framework to track the element globally and trigger a deactivate/reactivate dance. Use them deliberately (e.g., preserving a video player while it changes parents), not as a default.
Profiling the Three Trees
In practice you observe these trees through DevTools and rebuild markers:
- Flutter Inspector shows the widget tree; toggle to see the render tree with sizes and constraints.
- Rebuild Stats / RepaintRainbow reveal which subtrees re-run
build()or repaint. - Wrapping with
RepaintBoundaryisolates a render-object subtree into its own layer so its repaints don't cascade.
The mental model: too many widgets rebuilding is a reconciliation cost; too many render objects repainting is a raster cost. They are different problems with different fixes.
class IsolatedChart extends StatelessWidget {
const IsolatedChart({super.key, required this.painter});
final CustomPainter painter;
@override
Widget build(BuildContext context) {
// RepaintBoundary gives this CustomPaint its own render layer,
// so frequent repaints here don't dirty the parent's layer.
return RepaintBoundary(
child: CustomPaint(painter: painter),
);
}
}Quick Check: Reconciliation Outcome
A StatefulWidget of type EditorPane with no key is at position 0 of a Column. On the next rebuild, the parent returns a PreviewPane (a different type) at position 0 instead. What happens to the original EditorPane's element and State?
Recap: The Three Trees
You now have a working model of how Flutter reconciles UI:
- Widgets are immutable, throwaway blueprints — cheap to recreate every build.
- Elements are the mutable, long-lived tree that holds
Stateand runs reconciliation viaupdateChild. - RenderObjects are the expensive layout/paint objects Flutter strives to update in place.
- Reuse is decided by
canUpdate: equalruntimeTypeandkeymeans update in place; otherwise the subtree (and its state) is rebuilt. constidentity short-circuits reconciliation;Keys control sibling identity;GlobalKeys move elements across the tree;RepaintBoundaryisolates repaint cost.
With this mental model, performance work becomes precise: minimize unnecessary rebuilds (reconciliation cost) and unnecessary repaints (raster cost) separately.
الأسئلة الشائعة
هل درس «الأشجار الثلاثة: Widget وElement وRenderObject» مجاني؟
نعم — نص درس «الأشجار الثلاثة: Widget وElement وRenderObject» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Flutter Mobile Development، انتقل إلى CoddyKit PRO. تتضمن دورة Flutter Mobile Development 4 دروس في المجموع.
ماذا ستتعلم في «الأشجار الثلاثة: Widget وElement وRenderObject»؟
افهم كيفية مواءمة Flutter لأشجار widget وelement وrender أثناء عمليات إعادة البناء تتمرن على Flutter Mobile Development مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Flutter Mobile Development؟
لا تُشترط خبرة سابقة. Flutter Mobile Development على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «الأشجار الثلاثة: Widget وElement وRenderObject»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Flutter Mobile Development هذا؟
نعم. كل درس في Flutter Mobile Development يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- الأشجار الثلاثة: Widget وElement وRenderObject
- تحليل التقطّع باستخدام المخطط الزمني في DevTools
- RepaintBoundary وعناصر Const وتقليص إعادة البناء
- الإحماء المسبق للمظللات والترحيل إلى Impeller