Flutter Custom RenderObjects у 2026: Власне малювання та питання на співбесіді
Повний посібник зі створення власних RenderObjects у Flutter. Архітектура трьох дерев, імплементація RenderBox, піксельне малювання та підготовка до технічних співбесід.

Власні RenderObjects у Flutter забезпечують найнижчий рівень доступу до конвеєра рендерингу фреймворку, надаючи піксельну точність контролю над компонуванням, малюванням та обробкою дотиків, якої стандартні віджети не можуть запропонувати. Розуміння цього рівня відрізняє розробників, які створюють застосунки на Flutter, від тих, хто створює сам Flutter.
Позиції senior Flutter очікують від кандидатів пояснення трьох дерев (Widget, Element, RenderObject), визначення коли власний RenderObject перевершує CustomPainter та демонстрації робочої імплементації, що коректно обробляє constraints та hit testing.
Архітектура трьох дерев, що керує рендерингом Flutter
Flutter рендерить кадри через три взаємопов'язані дерева, кожне з окремими обов'язками. Дерево Widget містить незмінні конфігураційні об'єкти, які пишуть розробники. Дерево Element діє як постійний міст, що переживає перебудови та керує життєвим циклом. Дерево RenderObject виконує фактичні обчислення: компонування, малювання та hit testing.
Ця відмінність важлива для продуктивності. Коли setState ініціює перебудову, Flutter проходить дерево Element і порівнює нове дерево Widget зі старим. Лише змінені Elements створюють нові RenderObjects або оновлюють існуючі. Цей механізм reconciliation пояснює, чому Flutter може перебудовувати віджети 60 разів на секунду без втрати кадрів.
// Мінімальна структура widget-element-renderobject
class GaugeWidget extends LeafRenderObjectWidget {
const GaugeWidget({super.key, required this.value});
final double value; // від 0.0 до 1.0
RenderObject createRenderObject(BuildContext context) {
return RenderGauge(value: value);
}
void updateRenderObject(BuildContext context, RenderGauge renderObject) {
renderObject.value = value; // Оновлення без перестворення
}
}Базовий клас LeafRenderObjectWidget сигналізує, що цей віджет не має дочірніх елементів. Коли значення змінюється, updateRenderObject мутує існуючий екземпляр RenderGauge замість його заміни, зберігаючи всі закешовані обчислення компонування.
RenderBox vs RenderObject: вибір правильного базового класу
Більшість сценаріїв власного рендерингу повинні розширювати RenderBox, а не безпосередньо RenderObject. RenderBox імплементує декартову коробкову модель з обмеженнями ширини та висоти, що відповідає 99% випадків використання в мобільних та веб-застосунках. Документація класу RenderObject рекомендує безпосередньо наслідувати RenderObject лише при побудові фундаментально іншого протоколу компонування, такого як полярна система координат.
| Базовий клас | Коли використовувати | Приклади |
|---|---|---|
| RenderBox | Стандартні 2D компонування з коробковими обмеженнями | Власні діаграми, індикатори, поверхні малювання |
| RenderSliver | Прокручуваний контент з ледачим завантаженням на основі viewport | Власні заголовки списків, ефекти паралакса |
| RenderObject | Власні протоколи компонування (рідко) | Полярні компонування, власні системи координат |
class RenderGauge extends RenderBox {
RenderGauge({required double value}) : _value = value;
double _value;
double get value => _value;
set value(double newValue) {
if (_value == newValue) return;
_value = newValue;
markNeedsPaint(); // Лише перемальовування, без компонування
}
void performLayout() {
// Приймаємо найбільший дозволений розмір зберігаючи квадратні пропорції
final shortestSide = constraints.biggest.shortestSide;
size = Size.square(shortestSide);
}
void paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;
final center = offset + Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 10;
// Фонова дуга
final backgroundPaint = Paint()
..color = const Color(0xFFE0E0E0)
..style = PaintingStyle.stroke
..strokeWidth = 20
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
2.4, // Початковий кут (радіани)
4.9, // Кут розгортки (радіани)
false,
backgroundPaint,
);
// Дуга значення
final valuePaint = Paint()
..color = const Color(0xFF4CAF50)
..style = PaintingStyle.stroke
..strokeWidth = 20
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
2.4,
4.9 * _value, // Часткове заповнення
false,
valuePaint,
);
}
bool hitTestSelf(Offset position) => true;
}Метод performLayout повинен встановити властивість size в межах наданих constraints. Виклик markNeedsPaint() у сеттері забезпечує, що зміни значення ініціюють перемальовування без перерахунку компонування, оптимізуючи продуктивність під час анімацій.
Система constraints: як батьки комунікують з дочірніми елементами
Flutter використовує модель "constraints йдуть вниз, розміри йдуть вгору" для компонування. Батьки передають BoxConstraints дочірнім елементам, а дочірні повертають свій обраний Size. Цей односпрямований потік дозволяє обчислення компонування за час O(n) без множинних проходів.
// Розуміння BoxConstraints
void performLayout() {
// constraints.minWidth: мінімальна ширина яку дочірній елемент повинен мати
// constraints.maxWidth: максимальна ширина яку дочірній елемент може мати
// constraints.minHeight: мінімальна висота яку дочірній елемент повинен мати
// constraints.maxHeight: максимальна висота яку дочірній елемент може мати
// Tight constraints: min == max (точний розмір вимагається)
if (constraints.isTight) {
size = constraints.smallest;
return;
}
// Loose constraints: min < max (діапазон дозволений)
final preferredWidth = 200.0;
final preferredHeight = 100.0;
size = constraints.constrain(Size(preferredWidth, preferredHeight));
}Питання на співбесідах часто перевіряють розуміння цієї системи. Кандидати повинні пояснити, чому double.infinity як maxWidth не спричиняє помилок (RenderBox вибирає скінченний розмір) та як tight constraints примушують точні розміри.
Імплементація hit testing для інтерактивних компонентів
Hit testing визначає, які RenderObjects отримують події вказівника. Стандартна імплементація перевіряє, чи точка знаходиться в межах коробки, але власні форми потребують перевизначення.
class RenderGauge extends RenderBox {
// ... попередній код ...
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (!hitTestSelf(position)) return false;
result.add(BoxHitTestEntry(this, position));
return true;
}
bool hitTestSelf(Offset position) {
// Круговий hit test замість прямокутного
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
final distance = (position - center).distance;
return distance <= radius;
}
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is PointerDownEvent) {
// Обробка дотику
_onTapDown(entry.localPosition);
}
}
void _onTapDown(Offset localPosition) {
// Обчислюємо кут дотику та відповідно оновлюємо значення
final center = Offset(size.width / 2, size.height / 2);
final angle = (localPosition - center).direction;
// Конвертуємо кут у значення 0.0-1.0
}
}Hit testing проходить від кореня до листя. Кожен RenderObject вирішує, поглинути подію чи дозволити їй поширюватися далі. Розуміння цього поширення необхідне при побудові перекриваючих інтерактивних елементів.
CustomPainter vs RenderObject: коли використовувати що
CustomPainter пропонує простіший API для власного малювання, але RenderObject забезпечує більше контролю. Вибір залежить від вимог:
| Властивість | CustomPainter | RenderObject |
|---|---|---|
| Власна логіка компонування | Ні | Так |
| Власний hit testing | Обмежений | Повний контроль |
| Композиція шарів | Ні | Так |
| Підтримка дочірніх елементів | Ні | Так |
| Складність імплементації | Низька | Висока |
// Коли CustomPainter достатньо
class SimpleChartPainter extends CustomPainter {
final List<double> data;
SimpleChartPainter(this.data);
void paint(Canvas canvas, Size size) {
// Просте малювання без потреби власного компонування
final paint = Paint()..color = Colors.blue;
for (var i = 0; i < data.length; i++) {
final x = i * (size.width / data.length);
final height = data[i] * size.height;
canvas.drawRect(
Rect.fromLTWH(x, size.height - height, size.width / data.length - 2, height),
paint,
);
}
}
bool shouldRepaint(SimpleChartPainter oldDelegate) => data != oldDelegate.data;
}CustomPainter працює всередині стандартного RenderBox, наданого віджетом CustomPaint. Коли потрібен контроль над компонуванням, hit testing або композицією шарів, RenderObject стає необхідним.
Оптимізація продуктивності: markNeedsLayout vs markNeedsPaint
Розрізнення цих методів безпосередньо впливає на продуктивність. markNeedsLayout() інвалідує обчислення розміру та позиції, примушуючи прохід компонування. markNeedsPaint() лише позначає RenderObject для перемальовування без перерахунку компонування.
class RenderOptimizedGauge extends RenderBox {
double _value;
Color _color;
double _strokeWidth;
set value(double newValue) {
if (_value == newValue) return;
_value = newValue;
markNeedsPaint(); // Лише перемальовування
}
set color(Color newColor) {
if (_color == newColor) return;
_color = newColor;
markNeedsPaint(); // Лише перемальовування
}
set strokeWidth(double newWidth) {
if (_strokeWidth == newWidth) return;
_strokeWidth = newWidth;
markNeedsLayout(); // Може вплинути на розмір, потрібне компонування
}
}Виклик неправильного методу марнує цикли CPU. Анімації, що змінюють лише колір або значення, повинні використовувати markNeedsPaint(). Зміни, що впливають на розмір компонента, потребують markNeedsLayout().
Композиція шарів для складних ефектів
Шари Flutter дозволяють просунуті ефекти, такі як обрізання, трансформації та візуальні ефекти без впливу на продуктивність малювання.
class RenderClippedGauge extends RenderBox {
void paint(PaintingContext context, Offset offset) {
// Обрізання до кола
context.pushClipPath(
needsCompositing,
offset,
Offset.zero & size,
Path()..addOval(Offset.zero & size),
(context, offset) {
// Малювання всередині обрізаної області
_paintGauge(context.canvas, offset);
},
);
}
bool get needsCompositing => true; // Примушує окремий шар
void _paintGauge(Canvas canvas, Offset offset) {
// Фактичне малювання індикатора
}
}Прапорець needsCompositing інформує Flutter, що цей RenderObject потребує власного шару композиції. Це необхідно для ефектів на рівні шару, таких як opacity, clip та transform.
Часті питання на співбесідах
Під час технічних співбесід на позиції senior Flutter рекрутери часто задають такі питання:
-
Поясніть різницю між Widget, Element та RenderObject. Widgets — це незмінні конфігурації. Elements — це постійні мости, що керують життєвим циклом. RenderObjects виконують фактичний рендеринг.
-
Коли використовувати RenderObject замість CustomPainter? Коли потрібна власна логіка компонування, повний контроль над hit testing або композиція шарів.
-
Як працює система constraints у Flutter? Батьки передають BoxConstraints дочірнім елементам, дочірні вибирають розмір в цих межах та повертають його батькам. Односпрямований потік забезпечує компонування O(n).
-
Яка різниця між markNeedsLayout() та markNeedsPaint()? markNeedsLayout() примушує перерахунок розміру та позиції. markNeedsPaint() лише позначає для перемальовування. Використання неправильного методу впливає на продуктивність.
-
Як імплементувати круговий hit testing? Перевизначити hitTestSelf() та перевіряти відстань від центру замість використання стандартного прямокутного тесту.
Готовий до співбесід з Flutter?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Висновок
Оволодіння власними RenderObjects відкриває повні можливості рушія рендерингу Flutter. Розуміння архітектури трьох дерев, системи constraints та оптимізації продуктивності дозволяє створювати компоненти, що виходять за межі можливостей стандартних віджетів. Ці навички високо цінуються під час технічних співбесід на позиції senior Flutter і виділяють кандидатів, які знають фреймворк на глибокому рівні.
Чи знайдеш ти помилку в Flutter?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 11 вересня 2026 р.
Теги
Поділитися
Пов'язані статті

Flutter vs React Native у 2026: Архітектура, Продуктивність та Коли Обирати Кожен Фреймворк
Детальне порівняння Flutter 3.44 та React Native 0.86, що охоплює архітектуру рендерингу, бенчмарки продуктивності, досвід розробника та питання найму для кросплатформної мобільної розробки у 2026.

Flutter Navigation 2.0 та GoRouter у 2026: Deep Linking та питання співбесід
Опанування навігації Flutter з GoRouter 17.5: декларативний роутинг, deep linking, ShellRoute, guard'и маршрутів та питання співбесід з практичними прикладами.

Тестування Flutter у 2026: Widget Tests, Integration Tests та Golden Tests для технічних співбесід
Практичний посібник з тестування Flutter-застосунків: widget-тести, мокування з Mocktail, інтеграційне тестування, golden-тести та стратегії відповідей на типові запитання технічних співбесід 2026 року.