0Pricing
Flutter Mobile Development · Урок

Три дерева: Widget, Element и RenderObject

Изучите, как Flutter согласует деревья виджетов, элементов и отрисовки во время перестроений.

«Три дерева: 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 a StatelessWidget.
  • StatefulElement — owns the State object, 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 RepaintBoundary isolates 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 State and runs reconciliation via updateChild.
  • RenderObjects are the expensive layout/paint objects Flutter strives to update in place.
  • Reuse is decided by canUpdate: equal runtimeType and key means update in place; otherwise the subtree (and its state) is rebuilt.
  • const identity short-circuits reconciliation; Keys control sibling identity; GlobalKeys move elements across the tree; RepaintBoundary isolates 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 согласует деревья виджетов, элементов и отрисовки во время перестроений. Ты практикуешь 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 включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Три дерева: Widget, Element и RenderObject
  2. Профилирование рывков с помощью временной шкалы DevTools
  3. RepaintBoundary, константные виджеты и сокращение перестроений
  4. Прогрев шейдеров и миграция на Impeller
← Назад к Flutter Mobile Development