# Flutter Custom Render Objects ปี 2026: Custom Painting และคำถามสัมภาษณ์ > เชี่ยวชาญ rendering pipeline ของ Flutter ด้วย custom RenderObjects เรียนรู้ว่าควรเลือก CustomPainter หรือ RenderBox เมื่อใด และเตรียมตัวสำหรับคำถามสัมภาษณ์ระดับ senior เกี่ยวกับ lifecycle ของ Element-RenderObject - Published: 2026-09-11 - Updated: 2026-09-11 - Author: Anthony Fillion-Maillet - Tags: flutter, rendering, custom-painter, render-object, dart, mobile, interview - Reading time: 10 min --- Flutter custom RenderObjects ให้การเข้าถึงระดับต่ำสุดไปยัง rendering pipeline ของ framework ทำให้สามารถควบคุม pixel-perfect สำหรับ layout, painting และ hit testing ที่ widget มาตรฐานไม่สามารถให้ได้ การเข้าใจ layer นี้แยกผู้สมัครที่สร้างแอป Flutter จากผู้ที่สร้าง Flutter เอง > **สัญญาณการสัมภาษณ์** > > ตำแหน่ง Flutter ระดับ senior คาดหวังให้ผู้สมัครอธิบาย three trees (Widget, Element, RenderObject) อธิบายว่าเมื่อใด custom RenderObject จะดีกว่า CustomPainter และแสดง implementation ที่ใช้งานได้ซึ่งจัดการ constraints และ hit testing อย่างถูกต้อง ## สถาปัตยกรรม three-tree ที่ขับเคลื่อน rendering ของ Flutter Flutter render frames ผ่าน three trees ที่เชื่อมต่อกัน แต่ละ tree มีความรับผิดชอบที่แตกต่างกัน Widget tree ประกอบด้วย configuration objects แบบ immutable ที่ developer เขียน Element tree ทำหน้าที่เป็นสะพานเชื่อม persistent ที่อยู่รอดจาก rebuilds และจัดการ lifecycle RenderObject tree ทำการคำนวณจริง: layout, painting และ hit testing ความแตกต่างนี้สำคัญสำหรับ performance เมื่อ `setState` trigger rebuild Flutter จะเดินผ่าน Element tree และ diff Widget tree ใหม่กับ tree เก่า เฉพาะ Elements ที่เปลี่ยนแปลงเท่านั้นที่จะสร้าง RenderObjects ใหม่หรืออัปเดตที่มีอยู่ กลไก reconciliation นี้อธิบายว่าทำไม Flutter สามารถ rebuild widgets 60 ครั้งต่อวินาทีโดยไม่ drop frames ```dart // custom_gauge_widget.dart // A minimal widget-element-renderobject structure class GaugeWidget extends LeafRenderObjectWidget { const GaugeWidget({super.key, required this.value}); final double value; // 0.0 to 1.0 @override RenderObject createRenderObject(BuildContext context) { return RenderGauge(value: value); } @override void updateRenderObject(BuildContext context, RenderGauge renderObject) { renderObject.value = value; // Update without recreating } } ``` Base class `LeafRenderObjectWidget` บ่งบอกว่า widget นี้ไม่มี children เมื่อ value เปลี่ยน `updateRenderObject` จะ mutate instance RenderGauge ที่มีอยู่แทนที่จะแทนที่มัน รักษา layout computations ที่ถูก cache ไว้ ## RenderBox vs RenderObject: การเลือก base class ที่เหมาะสม สถานการณ์ custom rendering ส่วนใหญ่ควร extend `RenderBox` ไม่ใช่ `RenderObject` โดยตรง RenderBox implement โมเดล Cartesian box ด้วย constraints ของ width และ height ซึ่งตรงกับ 99% ของ use cases ในแอปพลิเคชัน mobile และ web [Documentation ของ RenderObject class](https://api.flutter.dev/flutter/rendering/RenderObject-class.html) แนะนำให้ subclass RenderObject โดยตรงเฉพาะเมื่อสร้าง layout protocol ที่แตกต่างโดยพื้นฐาน เช่น ระบบพิกัด polar | Base Class | เมื่อใดควรใช้ | ตัวอย่าง | |------------|---------------|----------| | RenderBox | Layout 2D มาตรฐานด้วย box constraints | Custom chart, gauge, drawing surface | | RenderSliver | เนื้อหา scrollable ด้วย lazy loading ตาม viewport | Custom list header, เอฟเฟกต์ parallax | | RenderObject | ระบบพิกัด non-Cartesian | Menu radial, polar chart | คำถามสัมภาษณ์ "เมื่อใดคุณจะ subclass RenderObject โดยตรง?" ทดสอบว่าผู้สมัครเข้าใจหรือไม่ว่า RenderBox ไม่ใช่ตัวเลือกเดียว แต่เป็นตัวเลือกที่ถูกต้องสำหรับปัญหาส่วนใหญ่ ## การ implement custom RenderBox: ตัวอย่าง gauge Widget gauge ในทางปฏิบัติแสดงให้เห็นสาม methods ที่ทุก custom RenderBox ต้องจัดการ: `performLayout`, `paint` และ hit testing Gauge รับ value จาก 0.0 ถึง 1.0 และวาด arc ที่แทนเปอร์เซ็นต์นั้น ```dart // render_gauge.dart 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(); // Repaint only, layout unchanged } @override void performLayout() { // Accept whatever size the parent offers, or use a default size = constraints.constrain(const Size(200, 200)); } @override void paint(PaintingContext context, Offset offset) { final canvas = context.canvas; final rect = offset & size; final center = rect.center; final radius = size.shortestSide / 2 - 10; // Background arc (gray) final backgroundPaint = Paint() ..color = const Color(0xFFE0E0E0) ..style = PaintingStyle.stroke ..strokeWidth = 8 ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), 2.4, // Start angle (roughly 7 o'clock) 4.9, // Sweep angle (to 5 o'clock) false, backgroundPaint, ); // Foreground arc (colored, proportional to value) final foregroundPaint = Paint() ..color = const Color(0xFF2196F3) ..style = PaintingStyle.stroke ..strokeWidth = 8 ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), 2.4, 4.9 * _value, // Sweep proportional to value false, foregroundPaint, ); } @override bool hitTestSelf(Offset position) => true; // Accept taps anywhere in bounds } ``` การเรียก `markNeedsPaint` เมื่อ value เปลี่ยนเป็นไปโดยเจตนา การเรียก `markNeedsLayout` จะเป็นการสิ้นเปลืองเพราะขนาดของ gauge ไม่ขึ้นอยู่กับ value ความแตกต่างนี้เป็นคำถามสัมภาษณ์ทั่วไป: "เมื่อใดคุณจะเรียก markNeedsLayout เทียบกับ markNeedsPaint?" ## CustomPainter vs custom RenderObject: กรอบการตัดสินใจ CustomPainter wrap canvas และ delegate painting ไปยัง class แยกต่างหาก Custom RenderObject ควบคุม layout, painting และ hit testing เป็นหน่วยเดียว Trade-off คือความซับซ้อนเทียบกับการควบคุม CustomPainter เหมาะเมื่อ: - Layout ถูกจัดการโดย widget parent แล้ว (ปกติคือ SizedBox หรือ Container) - ไม่ต้องการ custom hit testing หรือพื้นที่ที่ถูก paint ทั้งหมดสามารถ tap ได้ - Logic ของ painting เป็น stateless หรือขับเคลื่อนโดย single value Custom RenderObject เหมาะเมื่อ: - Layout ขึ้นอยู่กับการคำนวณภายใน (intrinsic sizing, baseline alignment) - Hit testing ต้องแม่นยำกับ shapes ที่ถูก paint ไม่ใช่ bounding box - Performance ต้องการข้าม pass layout ที่ไม่จำเป็น - Widget ต้องมีส่วนร่วมใน animations ที่ระดับ render ```dart // custom_painter_approach.dart // Simpler, but layout and hit testing are external class GaugePainter extends CustomPainter { final double value; GaugePainter(this.value); @override void paint(Canvas canvas, Size size) { // Same arc-drawing logic } @override bool shouldRepaint(GaugePainter old) => old.value != value; } // Usage requires explicit sizing SizedBox( width: 200, height: 200, child: CustomPaint( painter: GaugePainter(0.75), ), ) ``` วิธี CustomPainter สร้าง widgets มากขึ้น (SizedBox, CustomPaint) และผลักความรับผิดชอบ layout ไปยัง caller สำหรับ component gauge ที่สามารถใช้ซ้ำได้ เวอร์ชัน RenderBox encapsulate behavior ได้ดีกว่า ## การส่งต่อ constraints และ intrinsic dimensions Constraints ไหลลงไปใน render tree จาก parent ไปยัง child Sizes ไหลขึ้นจาก child ไปยัง parent Protocol สองทางนี้เป็นกระดูกสันหลังของ layout algorithm และเป็นหัวข้อสัมภาษณ์ที่พบบ่อย RenderBox ได้รับ `BoxConstraints` ที่ประกอบด้วย minimum และ maximum widths และ heights Method `performLayout` ต้องตั้ง `size` เป็นค่าภายใน constraints เหล่านั้น การละเมิด constraints จะสร้าง debug assertions ใน development และ behavior ที่ไม่แน่นอนใน production ```dart // render_gauge.dart (extended) class RenderGauge extends RenderBox { // ... previous code ... @override double computeMinIntrinsicWidth(double height) => 100; @override double computeMaxIntrinsicWidth(double height) => 300; @override double computeMinIntrinsicHeight(double width) => 100; @override double computeMaxIntrinsicHeight(double width) => 300; } ``` Intrinsic dimensions ตอบคำถาม: "render object นี้อยากจะใหญ่แค่ไหน โดยไม่สนใจ constraints?" Widgets เช่น `IntrinsicWidth` และ `IntrinsicHeight` query methods เหล่านี้เพื่อกำหนดขนาด children ของพวกมัน การ implement อย่างถูกต้องทำให้ gauge สามารถมีส่วนร่วมใน layouts ที่ยืดหยุ่นโดยไม่ต้องระบุขนาดโดยตรง ## Hit testing ด้วยความแม่นยำ Hit testing เริ่มต้นสำหรับ RenderBox ตรวจสอบว่าตำแหน่ง tap อยู่ภายใน bounding rectangle หรือไม่ สำหรับ gauge ที่มี shape เป็น arc hit testing ที่แม่นยำต้องการ override `hitTest` หรือ `hitTestSelf` ```dart // render_gauge.dart (hit testing) @override bool hitTestSelf(Offset position) { final center = size.center(Offset.zero); final radius = size.shortestSide / 2 - 10; final distanceFromCenter = (position - center).distance; // Only register hits on the arc stroke, not the center return distanceFromCenter >= radius - 10 && distanceFromCenter <= radius + 10; } ``` Implementation นี้ปฏิเสธ taps ตรงกลาง gauge โดยยอมรับเฉพาะที่ใกล้ arc เท่านั้น คำถามติดตามในการสัมภาษณ์: "คุณจะทำให้ gauge นี้ draggable เพื่อเปลี่ยน value ได้อย่างไร?" คำตอบ: implement `handleEvent` และแปลงตำแหน่ง tap เป็นมุม จากนั้นเป็น value ## Lifecycle ของ Element และการ attach RenderObject Elements สร้างและเป็นเจ้าของ RenderObjects Lifecycle methods `mount`, `update` และ `unmount` บน Element สอดคล้องกับ `attach`, render object ที่มีอยู่ และ `detach` ฝั่ง RenderObject การเข้าใจ lifecycle นี้อธิบาย memory management และ resource cleanup เมื่อ Element mount มันจะเรียก `createRenderObject` บน Widget ของมัน RenderObject ที่ถูก return จะถูก attach เข้ากับ render tree ผ่าน `attach` ซึ่งเชื่อมต่อกับ `PipelineOwner` สำหรับการ schedule layout และ paint เมื่อ Element unmount `detach` จะตัดการเชื่อมต่อ RenderObject และ `dispose` จะปล่อย resources ```dart // render_gauge.dart (resource cleanup) class RenderGauge extends RenderBox { // ... previous code ... ui.Image? _cachedBackground; @override void dispose() { _cachedBackground?.dispose(); // Release GPU resources _cachedBackground = null; super.dispose(); } } ``` การไม่ dispose GPU resources (Image, Picture, Layer) ทำให้เกิด memory leaks ที่สะสมเมื่อ widgets เข้าและออกจาก tree นี่เป็นข้อกังวลระดับ production ที่แยก implementations ที่ polish แล้วจาก tutorials ## Performance: เมื่อ custom RenderObjects ทำได้ดีกว่า widget composition โมเดล widget composition ของ Flutter จัดการ UI patterns ส่วนใหญ่ได้อย่างมีประสิทธิภาพ Custom RenderObjects ให้ข้อได้เปรียบในสถานการณ์เฉพาะ: 1. **หลีกเลี่ยง rebuild overhead**: RenderObject ที่อัปเดตผ่าน `markNeedsPaint` ข้าม build phase โดยสิ้นเชิง ในขณะที่วิธีการที่ใช้ widget ต้อง rebuild และ diff 2. **Batch painting**: RenderObject เดียวที่ paint หลาย elements (chart ที่มี 10,000 data points) ทำได้ดีกว่า 10,000 widgets ที่แต่ละตัวมี RenderObject ของตัวเอง 3. **Custom layout protocols**: layouts ที่ละเมิดโมเดล constraint แบบ single-pass (การเจรจา two-pass, elements ที่ซ้อนทับ) ต้องการการควบคุมระดับ RenderObject > **การ optimize ก่อนเวลา** > > แอปส่วนใหญ่ไม่ต้องการ custom RenderObjects ควร profile ด้วย DevTools ก่อนที่จะลงไประดับ render Widget tree ที่มีโครงสร้างดีพร้อม const constructors มักจะทำได้ดีกว่า custom renderer ที่ implement ไม่ดี ## คำถามสัมภาษณ์เกี่ยวกับ rendering internals ของ Flutter การสัมภาษณ์ระดับ senior และ staff สำรวจ rendering pipeline เพราะมันเผยให้เห็นความลึกของความเข้าใจ framework คำถามทั่วไปและคำตอบที่คาดหวัง: **Q: อธิบาย three trees ใน Flutter** A: Widget (config แบบ immutable), Element (handle แบบ persistent, จัดการ lifecycle), RenderObject (layout, paint, hit test) Elements อยู่รอดจาก rebuilds และ diff widgets เพื่อลด RenderObject mutations **Q: เมื่อใดคุณจะเลือก custom RenderObject แทน CustomPainter?** A: เมื่อต้องการ custom intrinsic sizing, hit testing ที่แม่นยำเกินกว่า bounding box หรือการมีส่วนร่วมโดยตรงใน layout protocol CustomPainter delegate layout ไปยัง ancestors **Q: markNeedsLayout ทำอะไรที่ต่างจาก markNeedsPaint?** A: markNeedsLayout schedule layout pass (constraints, sizing) ตามด้วย paint markNeedsPaint schedule เฉพาะ paint เท่านั้น ข้าม layout ใช้ option ที่น้อยที่สุดเพื่อหลีกเลี่ยง computation ที่สูญเปล่า **Q: constraints ไหลอย่างไรใน Flutter layout?** A: ลงจาก parent ไปยัง child (constraints เข้า) ขึ้นจาก child ไปยัง parent (size ออก) Single-pass, O(n) traversal Parents สามารถทำให้ constraints เข้มงวดขึ้น; children ต้องเคารพมัน **Q: จะเกิดอะไรขึ้นถ้า RenderObject ละเมิด constraints ของมัน?** A: Debug mode throws assertion Release mode สร้าง behavior ที่ไม่แน่นอน โดยทั่วไปคือ clipping หรือ overflow โดยไม่มี warning ฝึกอธิบายคำตอบเหล่านี้อย่างกระชับ Interviewers ให้คุณค่ากับความชัดเจนมากกว่าความสมบูรณ์ ## การประยุกต์ custom rendering กับสถานการณ์สัมภาษณ์จริง ความรู้เกี่ยวกับ rendering layer ของ Flutter นำไปใช้โดยตรงกับ[การเตรียมสัมภาษณ์ Flutter](/technologies/flutter) ผู้สมัครที่สามารถวาด RenderBox subclass บน whiteboard อธิบาย constraint protocol และอภิปรายว่าเมื่อใดควรลงไปต่ำกว่า widget layer แสดงให้เห็นถึงความลึกที่ตำแหน่ง senior ต้องการ ประเด็นสำคัญ: - โมเดล three-tree (Widget, Element, RenderObject) ทำให้ diffing มีประสิทธิภาพและ GPU updates น้อยที่สุด - RenderBox จัดการ Cartesian layouts; subclass RenderObject โดยตรงเฉพาะสำหรับระบบพิกัดทางเลือก - CustomPainter เหมาะกับงาน painting-only; custom RenderObjects encapsulate layout, painting และ hit testing - Intrinsic dimensions ทำให้ composition ยืดหยุ่นกับ sizing widgets เช่น IntrinsicWidth - dispose() ต้องปล่อย GPU resources เพื่อป้องกัน memory leaks - markNeedsPaint ถูกกว่า markNeedsLayout; ใช้ option ที่น้อยที่สุด - Profile ก่อน optimize; widget composition มีประสิทธิภาพอยู่แล้วสำหรับ use cases ส่วนใหญ่ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/flutter/flutter-custom-render-objects-custom-painting-2026