[{"data":1,"prerenderedAt":78},["ShallowReactive",2],{"tutorial-renderobject-custom-e-sliverpersistentheader-in-flutter":3,"comments-tutorial-renderobject-custom-e-sliverpersistentheader-in-flutter":77},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},89,"RenderObject custom e SliverPersistentHeader in Flutter","renderobject-custom-e-sliverpersistentheader-in-flutter","Impara a scendere sotto il livello dei widget: crea un RenderBox personalizzato con layout e paint propri, e costruisci un header sliver collassabile con SliverPersistentHeaderDelegate.","Il layer dei widget di Flutter è comodo, ma a volte insufficiente: quando ti serve un algoritmo di layout che nessun widget esistente offre (un flow radiale, un badge che si posiziona in base al figlio, una barra che si deforma allo scroll) devi lavorare con il **render layer**.\n\nIn questo tutorial avanzato vedremo:\n\n- come funziona il protocollo di layout `RenderBox` (constraints down, size up, parent sets position);\n- come implementare un `RenderObject` custom con `SingleChildRenderObjectWidget` e `MultiChildRenderObjectWidget`;\n- come integrarlo in un `CustomScrollView` tramite `SliverPersistentHeader` e un delegate personalizzato;\n- come debuggare layout e repaint con `debugPaintSizeEnabled`, `RepaintBoundary` e i DevTools.\n\nPrerequisiti: buona dimestichezza con l'albero dei widget, `CustomPaint`, sliver e `Canvas`.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F0b2bdf7e-f8d2-4ee1-9bb5-5ece906516da.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1628900941064-ba8df8b51e4a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODg5Mjg0MTF8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Akhmad Muzakir","https:\u002F\u002Funsplash.com\u002F@muzakirakhmad","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fblack-pen-on-white-printer-paper-o776uI1UtuQ","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=LCVjBlhQqbw",{"youtube_id":17,"duration_seconds":18,"published_at":19},"LCVjBlhQqbw",162,"2026-09-09T09:20:04+00:00","advanced",45,"3.x","published","2026-09-09T04:33:31+00:00","RenderObject custom e SliverPersistentHeader Flutter","Guida avanzata a RenderBox custom, MultiChildRenderObjectWidget e SliverPersistentHeaderDelegate per layout e header collassabili in Flutter.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,49,56,63,70],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},592,"Capire il protocollo di layout dei RenderBox","Prima di scrivere codice, fissiamo le regole del render layer:\n\n1. **I constraints scendono**: il genitore passa un `BoxConstraints` (min\u002Fmax width e height).\n2. **Le dimensioni salgono**: il figlio scrive `size` rispettando i constraints ricevuti.\n3. **Il genitore posiziona**: la posizione del figlio vive nel `parentData` (tipicamente un `BoxParentData` con `offset`).\n\nDa qui derivano alcuni vincoli pratici:\n\n- in `performLayout` puoi leggere `child.size` **solo** se hai chiamato `layout(..., parentUsesSize: true)`;\n- se il tuo layout dipende dalla dimensione del figlio devi dichiararlo, altrimenti Flutter lancia un assert in debug;\n- `markNeedsLayout()` invalida il layout, `markNeedsPaint()` solo il disegno: usa il secondo quando cambia solo un colore o un offset di pittura.\n\nUn `RenderBox` minimale implementa `performLayout`, `paint`, `hitTest` e i metodi `computeMin\u002FMaxIntrinsicWidth\u002FHeight` se il widget può stare in contesti che interrogano le dimensioni intrinseche.","\u002F\u002F Scheletro di un RenderBox con un figlio\nclass RenderScaleBox extends RenderBox\n    with RenderObjectWithChildMixin\u003CRenderBox> {\n  RenderScaleBox({required double scale}) : _scale = scale;\n\n  double _scale;\n  double get scale => _scale;\n  set scale(double value) {\n    if (_scale == value) return;\n    _scale = value;\n    markNeedsLayout(); \u002F\u002F cambia la size => serve un nuovo layout\n  }\n\n  @override\n  void performLayout() {\n    final child = this.child;\n    if (child == null) {\n      size = constraints.smallest;\n      return;\n    }\n    \u002F\u002F parentUsesSize: true perché usiamo child.size qui sotto\n    child.layout(constraints.loosen(), parentUsesSize: true);\n    size = constraints.constrain(child.size * _scale);\n  }\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    final child = this.child;\n    if (child == null) return;\n    context.pushTransform(\n      needsCompositing,\n      offset,\n      Matrix4.diagonal3Values(_scale, _scale, 1.0),\n      (ctx, o) => ctx.paintChild(child, o),\n    );\n  }\n}","dart","Hai chiaro il ciclo constraints\u002Fsize\u002Foffset e sai quando invalidare layout o paint.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},593,2,"Esporre il RenderObject come widget","Un `RenderObject` non si usa direttamente nell'albero: serve un `RenderObjectWidget` che lo crei (`createRenderObject`) e lo aggiorni (`updateRenderObject`). Con un solo figlio si estende `SingleChildRenderObjectWidget`.\n\nPunti chiave:\n\n- `createRenderObject` viene chiamato una sola volta per elemento;\n- `updateRenderObject` viene chiamato a ogni rebuild: assegna solo le proprietà, i setter decidono se invalidare;\n- implementa `debugFillProperties` per vedere i valori nel Widget Inspector.\n\nAggiungiamo anche `hitTest`: senza di esso i tocchi non arrivano al figlio quando applichiamo una trasformazione.","class ScaleBox extends SingleChildRenderObjectWidget {\n  const ScaleBox({super.key, required this.scale, super.child});\n\n  final double scale;\n\n  @override\n  RenderScaleBox createRenderObject(BuildContext context) =>\n      RenderScaleBox(scale: scale);\n\n  @override\n  void updateRenderObject(BuildContext context, RenderScaleBox renderObject) {\n    renderObject.scale = scale;\n  }\n\n  @override\n  void debugFillProperties(DiagnosticPropertiesBuilder properties) {\n    super.debugFillProperties(properties);\n    properties.add(DoubleProperty('scale', scale));\n  }\n}\n\n\u002F\u002F da aggiungere in RenderScaleBox\n\u002F\u002F @override\n\u002F\u002F bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {\n\u002F\u002F   final child = this.child;\n\u002F\u002F   if (child == null) return false;\n\u002F\u002F   return result.addWithPaintTransform(\n\u002F\u002F     transform: Matrix4.diagonal3Values(_scale, _scale, 1.0),\n\u002F\u002F     position: position,\n\u002F\u002F     hitTest: (r, p) => child.hitTest(r, position: p),\n\u002F\u002F   );\n\u002F\u002F }","`ScaleBox(scale: 1.4, child: ElevatedButton(...))` mostra il figlio ingrandito e cliccabile.",{"id":50,"position":51,"title":52,"body":53,"code_snippet":54,"code_language":39,"expected_result":55,"demo_url":41,"video_url":41},594,3,"Layout multi-figlio: un RenderBox che dispone i figli ad arco","Per più figli si usa `MultiChildRenderObjectWidget` insieme a `ContainerRenderObjectMixin` e `RenderBoxContainerDefaultsMixin`. Ogni figlio ha un `parentData` in cui salviamo l'offset calcolato; se serviamo dati extra (peso, angolo) definiamo una sottoclasse di `ContainerBoxParentData`.\n\nQui distribuiamo i figli lungo un arco: ogni figlio viene misurato con constraints laschi e posizionato su una circonferenza. Il `paint` usa i default del mixin, che rispetta gli offset in `parentData`.","class ArcParentData extends ContainerBoxParentData\u003CRenderBox> {}\n\nclass RenderArcLayout extends RenderBox\n    with\n        ContainerRenderObjectMixin\u003CRenderBox, ArcParentData>,\n        RenderBoxContainerDefaultsMixin\u003CRenderBox, ArcParentData> {\n  RenderArcLayout({required double radius}) : _radius = radius;\n\n  double _radius;\n  set radius(double v) {\n    if (_radius == v) return;\n    _radius = v;\n    markNeedsLayout();\n  }\n\n  @override\n  void setupParentData(RenderObject child) {\n    if (child.parentData is! ArcParentData) child.parentData = ArcParentData();\n  }\n\n  @override\n  void performLayout() {\n    size = constraints.constrain(Size(_radius * 2, _radius + 40));\n    final count = childCount;\n    var i = 0;\n    var child = firstChild;\n    while (child != null) {\n      final data = child.parentData! as ArcParentData;\n      child.layout(const BoxConstraints.tightFor(width: 56, height: 56),\n          parentUsesSize: true);\n      final t = count == 1 ? 0.5 : i \u002F (count - 1);\n      final angle = math.pi * (1 - t); \u002F\u002F da 180° a 0°\n      final center = Offset(size.width \u002F 2, size.height - 20);\n      final pos = center +\n          Offset(math.cos(angle) * _radius, -math.sin(angle) * _radius);\n      data.offset = pos - Offset(child.size.width \u002F 2, child.size.height \u002F 2);\n      i++;\n      child = data.nextSibling;\n    }\n  }\n\n  @override\n  void paint(PaintingContext context, Offset offset) =>\n      defaultPaint(context, offset);\n\n  @override\n  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>\n      defaultHitTestChildren(result, position: position);\n}\n\nclass ArcLayout extends MultiChildRenderObjectWidget {\n  const ArcLayout({super.key, this.radius = 120, super.children = const []});\n  final double radius;\n\n  @override\n  RenderArcLayout createRenderObject(BuildContext context) =>\n      RenderArcLayout(radius: radius);\n\n  @override\n  void updateRenderObject(BuildContext context, RenderArcLayout renderObject) {\n    renderObject.radius = radius;\n  }\n}","I figli di `ArcLayout` si dispongono a semicerchio e restano interattivi.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":39,"expected_result":62,"demo_url":41,"video_url":41},595,4,"Un header collassabile con SliverPersistentHeaderDelegate","`SliverAppBar` è già un `SliverPersistentHeader` preconfezionato. Quando serve un comportamento diverso (progress bar che si riempie, titolo che migra, contenuto che cambia layout) conviene scrivere il delegate.\n\nAPI essenziali del delegate:\n\n- `minExtent` \u002F `maxExtent`: estensione minima e massima in pixel;\n- `build(context, shrinkOffset, overlapsContent)`: `shrinkOffset` va da 0 a `maxExtent - minExtent`;\n- `shouldRebuild(old)`: torna `true` solo se i parametri cambiano.\n\nAttenzione: il widget restituito da `build` deve poter occupare l'altezza corrente; usa `SizedBox.expand` o `Stack` con `fit: StackFit.expand`. Con `pinned: true` l'header resta agganciato, con `floating: true` ricompare allo scroll verso l'alto.","class CollapsingHeader extends SliverPersistentHeaderDelegate {\n  CollapsingHeader({required this.title, required this.progress});\n\n  final String title;\n  final double progress; \u002F\u002F 0..1\n\n  @override\n  double get minExtent => 72;\n  @override\n  double get maxExtent => 220;\n\n  @override\n  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {\n    final t = (shrinkOffset \u002F (maxExtent - minExtent)).clamp(0.0, 1.0);\n    final scheme = Theme.of(context).colorScheme;\n    return Material(\n      color: Color.lerp(scheme.primaryContainer, scheme.primary, t),\n      elevation: 4 * t,\n      child: SizedBox.expand(\n        child: Stack(\n          children: [\n            Positioned(\n              left: 16 + 8 * t,\n              bottom: lerpDouble(24, 20, t)!,\n              child: Text(\n                title,\n                style: TextStyle(\n                  fontSize: lerpDouble(30, 18, t)!,\n                  fontWeight: FontWeight.bold,\n                  color: Color.lerp(scheme.onPrimaryContainer, scheme.onPrimary, t),\n                ),\n              ),\n            ),\n            Align(\n              alignment: Alignment.bottomCenter,\n              child: LinearProgressIndicator(value: progress, minHeight: 3),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  @override\n  bool shouldRebuild(CollapsingHeader oldDelegate) =>\n      oldDelegate.title != title || oldDelegate.progress != progress;\n}","Scrollando, l'header si comprime da 220 a 72 px interpolando colore, elevazione e dimensione del titolo.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":39,"expected_result":69,"demo_url":41,"video_url":41},596,5,"Comporre tutto in un CustomScrollView","Mettiamo insieme header sliver e layout custom. Il `SliverPersistentHeader` è pinned; sotto inseriamo l'`ArcLayout` dentro un `SliverToBoxAdapter` e una lista.\n\nNota: `SliverToBoxAdapter` costruisce sempre il figlio, anche fuori viewport. Per contenuti costosi preferisci `SliverList`\u002F`SliverGrid` o incapsula in `RepaintBoundary` per isolare i repaint dell'header dal resto.","class DemoPage extends StatelessWidget {\n  const DemoPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: CustomScrollView(\n        slivers: [\n          SliverPersistentHeader(\n            pinned: true,\n            delegate: CollapsingHeader(title: 'Render layer', progress: 0.62),\n          ),\n          const SliverToBoxAdapter(\n            child: RepaintBoundary(\n              child: Padding(\n                padding: EdgeInsets.symmetric(vertical: 24),\n                child: ArcLayout(\n                  radius: 110,\n                  children: [\n                    Icon(Icons.home, size: 32),\n                    Icon(Icons.search, size: 32),\n                    Icon(Icons.star, size: 32),\n                    Icon(Icons.settings, size: 32),\n                  ],\n                ),\n              ),\n            ),\n          ),\n          SliverList.builder(\n            itemCount: 40,\n            itemBuilder: (_, i) => ListTile(title: Text('Elemento $i')),\n          ),\n        ],\n      ),\n    );\n  }\n}","Una schermata con header collassabile pinned, icone disposte ad arco e una lista scrollabile fluida.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":39,"expected_result":76,"demo_url":41,"video_url":41},597,6,"Debug, performance e checklist finale","Il render layer è potente ma facile da sbagliare. Strumenti e regole:\n\n**Debug visivo**\n\n```dart\nimport 'package:flutter\u002Frendering.dart';\nvoid main() {\n  debugPaintSizeEnabled = true;      \u002F\u002F bordi dei box\n  debugRepaintRainbowEnabled = true; \u002F\u002F chi ridisegna\n  runApp(const MyApp());\n}\n```\n\n**Checklist**\n\n- ogni setter pubblico invalida con `markNeedsLayout` **o** `markNeedsPaint`, mai entrambi inutilmente;\n- se usi `child.size` passa `parentUsesSize: true`;\n- implementa `computeDryLayout` se il tuo box può finire in un `IntrinsicHeight` o in una `Table`;\n- implementa `hitTestChildren` quando applichi trasformazioni o offset non banali;\n- aggiungi `Semantics` (o `describeSemanticsConfiguration`) per l'accessibilità: un render object custom è invisibile allo screen reader;\n- nel delegate sliver mantieni `shouldRebuild` il più selettivo possibile e non allocare oggetti costosi in `build`.\n\n**Verifica in profilo**: esegui `flutter run --profile` e controlla nei DevTools la timeline dei frame; se vedi picchi in *Layout*, probabilmente stai invalidando il layout quando basterebbe il paint.","@override\nSize computeDryLayout(BoxConstraints constraints) {\n  final child = this.child;\n  if (child == null) return constraints.smallest;\n  final childSize = child.getDryLayout(constraints.loosen());\n  return constraints.constrain(childSize * _scale);\n}","Layout corretto anche in contesti intrinseci, nessun assert in debug e frame stabili sotto i 16 ms.",[],1789120576226]