← Назад к вопросам

Что такое рендер дерево?

1.8 Middle🔥 111 комментариев
#Flutter виджеты#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Рендер дерево в Flutter

Определение

Рендер дерево (Render Tree) — это низкоуровневое представление UI, которое Flutter использует для рисования пикселей на экране. Это промежуточный слой между высокоуровневыми Widget'ами и реальным растеризацией пикселей.

Widget дерево → Render дерево → Painting → Display

Три слоя архитектуры Flutter

┌─────────────────────────────────────────────┐
│ Widget Layer (Declarative, immutable)      │
│ Text(), Button(), Column(), Row() ...      │
└──────────────┬──────────────────────────────┘
               │ buildChildOf()
┌──────────────▼──────────────────────────────┐
│ RenderObject Layer (Mutable, Imperative)    │
│ RenderParagraph, RenderBox, RenderFlex ... │
└──────────────┬──────────────────────────────┘
               │ paint(), layout()
┌──────────────▼──────────────────────────────┐
│ Engine Layer (Skia painting, compositing)   │
│ Canvas.drawPath(), drawImage() ...          │
└──────────────┬──────────────────────────────┘
               │
┌──────────────▼──────────────────────────────┐
│ Frame on screen                             │
└─────────────────────────────────────────────┘

Что такое RenderObject

RenderObject — это объект, который знает, как:

  • Вычислять размеры (layout)
  • Рисовать себя на экране (paint)
  • Обрабатывать hit testing (касание пальцем)
abstract class RenderObject extends AbstractNode {
  // Layout
  void performLayout() { ... }
  Size computeMinIntrinsicWidth(double height) { ... }
  
  // Paint
  void paint(PaintingContext context, Offset offset) { ... }
  
  // Hit testing
  bool hitTest(HitTestResult result, {required Offset position}) { ... }
}

Пример простого дерева

// Widget дерево
MyApp(
  child: Column(
    children: [
      Text('Hello'),
      ElevatedButton(onPressed: () {}, child: Text('Tap')),
    ],
  ),
)

// Соответствующее Render дерево
RenderView
├── RenderPadding
│   └── RenderFlex (Column)
│       ├── RenderParagraph (Text 'Hello')
│       └── RenderPhysicalShape (ElevatedButton)
│           └── RenderConstrainedBox
│               └── RenderFlex (Row for button content)
│                   └── RenderParagraph (Text 'Tap')

Процесс создания Render дерева

Шаг 1: Widget создание

Text('Hello') // это Widget, immutable

Шаг 2: Создание RenderObject

class Text extends Widget {
  @override
  RenderObjectWidget createRenderObject(BuildContext context) {
    return RenderParagraph(
      text,
      style: style,
      // ...
    );
  }
}

Шаг 3: Layout (вычисление размеров)

RenderFlex.performLayout() {
  // 1. Вычисляем доступное пространство
  // 2. Раскладываем детей (children) по оси Y
  // 3. Даём каждому ребёнку constraints
  // 4. Вызываем child.layout()
  
  for (var child in children) {
    child.layout(constraints, parentUsesSize: true);
    // Теперь child.size известен
  }
}

Шаг 4: Paint (рисование)

RenderParagraph.paint(context, offset) {
  // Рисуем текст в canvas
  canvas.drawParagraph(paragraph, offset);
}

RenderFlex.paint(context, offset) {
  // 1. Рисуем детей
  for (var child in children) {
    context.paintChild(child, childOffset);
  }
}

Key отличия Widget vs RenderObject

// Widget — description, immutable
class Text extends Widget {
  final String text;
  final TextStyle? style;  // immutable
  
  // Widget создаётся заново в каждом build()
}

// RenderObject — mutable state, reused
class RenderParagraph extends RenderBox {
  late Paragraph _paragraph;  // mutable
  
  // RenderObject переиспользуется, если типы совпадают
  // (это называется "inflate" в Flutter)
}

Когда вызывается setState() или изменяется Widget:

// 1. Создаётся новый Widget (старый выбрасывается)
Text('Hello') → Text('World')

// 2. RenderObject переиспользуется (если тип совпадает)
RenderParagraph остаётся тот же

// 3. RenderObject обновляется
renderParagraph.update(newWidget);

// 4. Вызывается markNeedsLayout() и markNeedsPaint()
// для перерасчёта layout и перерисовки

Layout constraints и sizes

Constraints — ограничения для детей:

class BoxConstraints {
  final double minWidth, maxWidth;
  final double minHeight, maxHeight;
}

// Пример: Column даёт каждому ребёнку constraints
RenderFlex.layout(constraints) {
  // Если Column шириной 300 и высотой 500
  // Каждый child получит:
  // width: 300 (max и min одинаковые)
  // height: ? (зависит от flex factor)
}

Size — реальный размер после layout:

RenderBox box;
box.size; // Size(300, 100) — вычисляется в performLayout()

Практический пример: как Flutter рисует экран

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  bool showDetails = false;
  
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text('Title'),
      if (showDetails) Text('Details'),
      ElevatedButton(
        onPressed: () => setState(() => showDetails = !showDetails),
        child: Text('Toggle'),
      ),
    ]);
  }
}

// Render дерево до setState:
RenderFlex(Column)
├── RenderParagraph('Title')
├── RenderPhysicalShape(Button)
└── ...

// После setState(() => showDetails = true):
RenderFlex(Column)  // переиспользуется, НО
├── RenderParagraph('Title')  // переиспользуется
├── RenderParagraph('Details')  // НОВЫЙ узел!
├── RenderPhysicalShape(Button)  // переиспользуется
└── ...

// Flutter вычисляет:
// 1. markNeedsLayout() для Column
// 2. performLayout() пересчитывает размеры
// 3. paint() перерисовывает

Оптимизация производительности

Проблема 1: Лишние rebuilds

// ❌ Плохо — TextWidget перестраивается, даже если text не изменился
class Parent extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return TextWidget(text: "Hello");
  }
}

// Parent rebuild → TextWidget rebuild → новое RenderObject?

Решение: const конструктор:

// ✅ Хорошо
const TextWidget(text: "Hello");
// Если текст не меняется, это один и тот же Widget объект
// RenderObject переиспользуется

Проблема 2: Большое дерево

// ❌ Плохо — весь список перестраивается
ListView(
  children: List.generate(1000, (i) => ListTile(...)),
)

// Render дерево: 1000 RenderObject'ов
// setState → пересчитываются размеры и позиции всех 1000

Решение: ListView.builder:

// ✅ Хорошо — только видимые items рендерятся
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) => ListTile(...),
)

Дебугирование render дерева

// DevTools → Inspector → Show render tree
// Или programmatically:

DebugPrintBeginFrame('Frame'); // вывести отладку

// В каждом RenderObject:
renderObject.toStringDeep(); // полное дерево

Выводы

  1. Widget дерево — то, что писал ты (declarative)
  2. Render дерево — как Flutter это реализует (imperative, optimized)
  3. RenderObject — умеет размещать (layout) и рисовать (paint)
  4. Переиспользование — один из ключей к производительности
  5. Constraints — система, которая делает layout эффективным
  6. const конструкторы и ListView.builder — оптимизируют render дерево

Понимание render дерева критично для оптимизации производительности Flutter приложений.