[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-layout-personalizzati-in-flutter-da-custommultichildlayout-ai-renderobject":3,"comments-article-layout-personalizzati-in-flutter-da-custommultichildlayout-ai-renderobject":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},93,"Layout personalizzati in Flutter: da CustomMultiChildLayout ai RenderObject","layout-personalizzati-in-flutter-da-custommultichildlayout-ai-renderobject","Quando Row, Column e Stack non bastano, Flutter offre strumenti di layout su misura: CustomSingleChildLayout, CustomMultiChildLayout, Flow e i RenderObject. Guida pratica al protocollo di layout e a quando usare ciascuna soluzione.","## Perché andare oltre Row, Column e Stack\n\nLa maggior parte delle interfacce Flutter si costruisce componendo widget di layout standard: `Row`, `Column`, `Stack`, `Wrap`, `Flex`. Prima o poi, però, capita un requisito che con la composizione diventa un labirinto di `LayoutBuilder`, `Positioned` e calcoli a mano: una griglia a cascata, un menu radiale, avatar sovrapposti con offset dinamico, un tooltip che si posiziona rispetto a un ancoraggio evitando i bordi dello schermo.\n\nIn questi casi Flutter mette a disposizione una scala di strumenti, dal più semplice al più potente:\n\n1. `CustomSingleChildLayout` — posiziona e vincola **un** figlio tramite un delegate.\n2. `CustomMultiChildLayout` — posiziona **più** figli identificati da `LayoutId`.\n3. `Flow` — layout basato su trasformazioni, ottimizzato per animazioni.\n4. `RenderObject` custom — controllo totale su misura, posizionamento, paint e hit test.\n\nPrima di scrivere codice, però, serve capire come Flutter calcola un layout.\n\n## Il protocollo di layout in tre regole\n\nIl motore di layout di Flutter si riassume in una frase spesso citata:\n\n> **I vincoli scendono, le dimensioni salgono, il genitore posiziona.**\n\nIn concreto:\n\n- Il genitore passa al figlio un `BoxConstraints` (minWidth, maxWidth, minHeight, maxHeight).\n- Il figlio sceglie la propria `Size` **rispettando** quei vincoli.\n- Il genitore decide l'**offset** del figlio al proprio interno.\n\nUn figlio non sa dove verrà posizionato, e il genitore non può imporre una dimensione se non tramite i vincoli. Questo passaggio unico dall'alto verso il basso e ritorno rende il layout lineare (O(n)) e molto veloce, ma spiega anche perché certe cose \"non si possono fare\": ad esempio un widget non può dimensionarsi in base alla posizione che assumerà.\n\nDa Flutter è disponibile anche il **dry layout** (`computeDryLayout` \u002F `getDryLayout`): un modo per chiedere \"che dimensione avresti con questi vincoli?\" senza eseguire davvero il layout. È fondamentale per implementare RenderObject corretti.\n\n## CustomSingleChildLayout: posizionare un figlio con logica propria\n\nÈ lo strumento giusto quando hai **un solo figlio** e devi decidere vincoli e posizione con una regola non banale. Esempio classico: un pannello ancorato a un punto, che si mantiene dentro i limiti del genitore.\n\n```dart\nclass AnchoredLayoutDelegate extends SingleChildLayoutDelegate {\n  AnchoredLayoutDelegate({required this.anchor, this.margin = 8});\n\n  \u002F\u002F\u002F Punto di ancoraggio in coordinate del genitore.\n  final Offset anchor;\n  final double margin;\n\n  @override\n  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {\n    \u002F\u002F Il figlio può essere al massimo grande quanto lo spazio meno i margini.\n    return constraints.loosen().copyWith(\n      maxWidth: constraints.maxWidth - margin * 2,\n      maxHeight: constraints.maxHeight - margin * 2,\n    );\n  }\n\n  @override\n  Offset getPositionForChild(Size size, Size childSize) {\n    \u002F\u002F Centro orizzontalmente sull'ancora, poi \"clamp\" dentro i bordi.\n    var dx = anchor.dx - childSize.width \u002F 2;\n    var dy = anchor.dy + margin;\n\n    dx = dx.clamp(margin, size.width - childSize.width - margin);\n    if (dy + childSize.height > size.height - margin) {\n      \u002F\u002F Non c'è spazio sotto: ribalto sopra l'ancora.\n      dy = anchor.dy - childSize.height - margin;\n    }\n    return Offset(dx, dy);\n  }\n\n  @override\n  bool shouldRelayout(AnchoredLayoutDelegate oldDelegate) =>\n      anchor != oldDelegate.anchor || margin != oldDelegate.margin;\n}\n```\n\nUso:\n\n```dart\nCustomSingleChildLayout(\n  delegate: AnchoredLayoutDelegate(anchor: tapPosition),\n  child: const _TooltipCard(),\n)\n```\n\nPunti chiave:\n\n- `getSize` (opzionale) definisce la dimensione del widget stesso; per default riempie i vincoli.\n- `shouldRelayout` deve tornare `true` **solo** quando i parametri del delegate cambiano davvero: è la tua ottimizzazione principale.\n\n## CustomMultiChildLayout: più figli, ognuno con il suo ruolo\n\nQuando i figli sono più di uno e hanno **ruoli semantici diversi**, `CustomMultiChildLayout` è la scelta naturale. Ogni figlio viene marcato con `LayoutId` e il delegate li misura e posiziona nell'ordine che preferisci — anche in funzione delle dimensioni degli altri.\n\nEsempio: un header in cui il titolo occupa lo spazio residuo lasciato da un badge e da un'azione.\n\n```dart\nenum _Slot { badge, title, action }\n\nclass HeaderLayoutDelegate extends MultiChildLayoutDelegate {\n  HeaderLayoutDelegate({required this.gap});\n\n  final double gap;\n\n  @override\n  void performLayout(Size size) {\n    var used = 0.0;\n    Size badgeSize = Size.zero;\n    Size actionSize = Size.zero;\n\n    if (hasChild(_Slot.badge)) {\n      badgeSize = layoutChild(\n        _Slot.badge,\n        BoxConstraints.loose(size),\n      );\n      used += badgeSize.width + gap;\n    }\n\n    if (hasChild(_Slot.action)) {\n      actionSize = layoutChild(\n        _Slot.action,\n        BoxConstraints.loose(size),\n      );\n      used += actionSize.width + gap;\n    }\n\n    if (hasChild(_Slot.title)) {\n      final titleSize = layoutChild(\n        _Slot.title,\n        BoxConstraints(\n          maxWidth: (size.width - used).clamp(0.0, size.width),\n          maxHeight: size.height,\n        ),\n      );\n      positionChild(\n        _Slot.title,\n        Offset(badgeSize.width + gap, (size.height - titleSize.height) \u002F 2),\n      );\n    }\n\n    if (hasChild(_Slot.badge)) {\n      positionChild(_Slot.badge, Offset(0, (size.height - badgeSize.height) \u002F 2));\n    }\n    if (hasChild(_Slot.action)) {\n      positionChild(\n        _Slot.action,\n        Offset(size.width - actionSize.width, (size.height - actionSize.height) \u002F 2),\n      );\n    }\n  }\n\n  @override\n  bool shouldRelayout(HeaderLayoutDelegate oldDelegate) => gap != oldDelegate.gap;\n}\n```\n\nE il widget:\n\n```dart\nSizedBox(\n  height: 56,\n  child: CustomMultiChildLayout(\n    delegate: HeaderLayoutDelegate(gap: 12),\n    children: const [\n      LayoutId(id: _Slot.badge, child: CircleAvatar(radius: 20)),\n      LayoutId(id: _Slot.title, child: Text('Titolo molto lungo…', maxLines: 1)),\n      LayoutId(id: _Slot.action, child: Icon(Icons.more_vert)),\n    ],\n  ),\n)\n```\n\nRegole da rispettare, altrimenti scattano gli assert in debug:\n\n- ogni figlio deve essere **misurato esattamente una volta** con `layoutChild`;\n- ogni figlio deve essere **posizionato** con `positionChild` (se non lo fai, finisce a `Offset.zero`);\n- il delegate **non** può leggere la dimensione di un figlio che non ha ancora misurato.\n\nUn limite importante: `CustomMultiChildLayout` non può dimensionarsi in base ai figli (la `size` è determinata dai vincoli ricevuti o da `getSize`). Se ti serve un contenitore che si adatta al contenuto, devi passare a un RenderObject custom.\n\n## Flow: layout e trasformazioni ottimizzati per l'animazione\n\n`Flow` è pensato per casi in cui la posizione dei figli cambia continuamente (menu a espansione, carousel, effetti). La sua caratteristica è che i figli vengono disegnati applicando una **matrice di trasformazione** in fase di paint: cambiare le posizioni **non ricalcola il layout**, cosa che lo rende molto efficiente per animazioni.\n\n```dart\nclass RadialFlowDelegate extends FlowDelegate {\n  RadialFlowDelegate({required this.animation, required this.radius})\n      : super(repaint: animation);\n\n  final Animation\u003Cdouble> animation;\n  final double radius;\n\n  @override\n  void paintChildren(FlowPaintingContext context) {\n    final count = context.childCount;\n    for (var i = 0; i \u003C count; i++) {\n      final angle = (math.pi \u002F (count - 1)) * i + math.pi; \u002F\u002F semicerchio\n      final distance = radius * animation.value;\n      final dx = context.size.width \u002F 2 + math.cos(angle) * distance;\n      final dy = context.size.height + math.sin(angle) * distance;\n\n      context.paintChild(\n        i,\n        transform: Matrix4.translationValues(\n          dx - context.getChildSize(i)!.width \u002F 2,\n          dy - context.getChildSize(i)!.height \u002F 2,\n          0,\n        ),\n      );\n    }\n  }\n\n  @override\n  bool shouldRepaint(RadialFlowDelegate oldDelegate) =>\n      animation != oldDelegate.animation || radius != oldDelegate.radius;\n}\n```\n\nAttenzione: con `Flow` i figli disegnati fuori dai limiti del widget possono essere tagliati e la gestione del tocco segue le trasformazioni applicate, quindi verifica sempre l'hit test con `debugPaintPointersEnabled` o provando l'interazione reale.\n\n## Il livello più basso: un RenderObject su misura\n\nQuando serve un contenitore che **si dimensiona in base ai figli**, che espone dimensioni intrinseche o che partecipa in modo particolare al paint, si scende al livello di `RenderBox`.\n\nEsempio concreto e molto richiesto: una lista di avatar sovrapposti, larga quanto serve, con overlap configurabile.\n\n### 1. Il widget\n\n```dart\nclass AvatarStack extends MultiChildRenderObjectWidget {\n  const AvatarStack({\n    super.key,\n    this.overlap = 0.35,\n    required super.children,\n  });\n\n  \u002F\u002F\u002F Percentuale di sovrapposizione (0 = nessuna, 0.5 = metà).\n  final double overlap;\n\n  @override\n  RenderAvatarStack createRenderObject(BuildContext context) =>\n      RenderAvatarStack(overlap: overlap);\n\n  @override\n  void updateRenderObject(BuildContext context, RenderAvatarStack renderObject) {\n    renderObject.overlap = overlap;\n  }\n}\n```\n\n### 2. Il RenderObject\n\n```dart\nclass _AvatarParentData extends ContainerBoxParentData\u003CRenderBox> {}\n\nclass RenderAvatarStack extends RenderBox\n    with\n        ContainerRenderObjectMixin\u003CRenderBox, _AvatarParentData>,\n        RenderBoxContainerDefaultsMixin\u003CRenderBox, _AvatarParentData> {\n  RenderAvatarStack({required double overlap}) : _overlap = overlap;\n\n  double _overlap;\n  double get overlap => _overlap;\n  set overlap(double value) {\n    if (_overlap == value) return;\n    _overlap = value;\n    markNeedsLayout(); \u002F\u002F richiede un nuovo layout, non solo un repaint\n  }\n\n  @override\n  void setupParentData(RenderBox child) {\n    if (child.parentData is! _AvatarParentData) {\n      child.parentData = _AvatarParentData();\n    }\n  }\n\n  @override\n  void performLayout() {\n    final childConstraints = BoxConstraints.loose(constraints.biggest);\n    var dx = 0.0;\n    var height = 0.0;\n\n    var child = firstChild;\n    while (child != null) {\n      child.layout(childConstraints, parentUsesSize: true);\n      final data = child.parentData! as _AvatarParentData;\n      data.offset = Offset(dx, 0);\n\n      dx += child.size.width * (1 - overlap);\n      height = math.max(height, child.size.height);\n      child = data.nextSibling;\n    }\n\n    \u002F\u002F Recupero la parte \"tagliata\" dall'ultimo elemento.\n    final width = childCount == 0\n        ? 0.0\n        : dx + (lastChild!.size.width * overlap);\n\n    size = constraints.constrain(Size(width, height));\n  }\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    defaultPaint(context, offset);\n  }\n\n  @override\n  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {\n    \u002F\u002F Testa i figli dall'ultimo al primo: l'avatar in cima riceve il tocco.\n    return defaultHitTestChildren(result, position: position);\n  }\n}\n```\n\nCosa sta succedendo:\n\n- `ContainerRenderObjectMixin` fornisce la lista concatenata dei figli (`firstChild`, `nextSibling`).\n- `parentUsesSize: true` indica che il genitore legge la `size` del figlio: senza questo flag Flutter non può ottimizzare correttamente i relayout boundary.\n- `size = constraints.constrain(...)` garantisce che la dimensione scelta rispetti i vincoli ricevuti.\n- `defaultPaint` e `defaultHitTestChildren` arrivano da `RenderBoxContainerDefaultsMixin` e gestiscono ordine di disegno e tocco.\n\n### 3. Dimensioni intrinseche e dry layout\n\nSe il tuo RenderObject verrà usato dentro `IntrinsicWidth`, `Row` con `IntrinsicHeight` o `Table`, conviene implementare anche gli intrinsics e il dry layout:\n\n```dart\n@override\ndouble computeMinIntrinsicHeight(double width) {\n  var height = 0.0;\n  var child = firstChild;\n  while (child != null) {\n    height = math.max(height, child.getMinIntrinsicHeight(width));\n    child = (child.parentData! as _AvatarParentData).nextSibling;\n  }\n  return height;\n}\n\n@override\nSize computeDryLayout(BoxConstraints constraints) {\n  var width = 0.0;\n  var height = 0.0;\n  RenderBox? child = firstChild;\n  Size lastSize = Size.zero;\n\n  while (child != null) {\n    lastSize = child.getDryLayout(BoxConstraints.loose(constraints.biggest));\n    width += lastSize.width * (1 - overlap);\n    height = math.max(height, lastSize.height);\n    child = (child.parentData! as _AvatarParentData).nextSibling;\n  }\n  if (childCount > 0) width += lastSize.width * overlap;\n  return constraints.constrain(Size(width, height));\n}\n```\n\nGli intrinsics sono costosi (possono far ripetere il layout più volte): implementali solo se servono davvero.\n\n## Come scegliere: una tabella mentale\n\n- **`Align`, `Stack` + `Positioned`, `Wrap`, `Flexible`** → prima scelta. Se la logica si esprime componendo, componi.\n- **`LayoutBuilder`** → quando basta conoscere i vincoli per scegliere fra due layout diversi. Attenzione: introduce un rebuild in fase di layout, evitalo dentro liste molto lunghe.\n- **`CustomSingleChildLayout`** → un figlio, posizionamento calcolato.\n- **`CustomMultiChildLayout`** → figli con ruoli distinti e dipendenze reciproche, dimensione del contenitore dettata dai vincoli.\n- **`Flow`** → posizioni animate frequentemente, ricalcolate solo in paint.\n- **`RenderObject`** → dimensione dipendente dai figli, intrinsics, paint o hit test personalizzati, layout con logiche esotiche (griglie a cascata, layout circolari con collisioni).\n\n## Errori comuni e come evitarli\n\n- **\"BoxConstraints forces an infinite width\"**: stai passando `constraints.biggest` in un contesto scrollabile orizzontalmente. Usa `constraints.loosen()` con un `maxWidth` finito o gestisci esplicitamente `isTight`\u002F`hasBoundedWidth`.\n- **Dimenticare `markNeedsLayout` \u002F `markNeedsPaint`** nei setter del RenderObject: la UI non si aggiorna. Regola pratica: se il valore cambia la geometria → `markNeedsLayout`; se cambia solo il disegno → `markNeedsPaint`.\n- **`shouldRelayout` che ritorna sempre `true`**: rilayout inutili a ogni frame.\n- **Misurare due volte lo stesso figlio** in `MultiChildLayoutDelegate`: assert in debug e comportamento imprevedibile in release.\n- **Ignorare i vincoli**: se non chiami `constraints.constrain(...)` rischi overflow o assert `RenderBox does not meet its constraints`.\n\n## Debug: gli strumenti che servono davvero\n\n- `debugPaintSizeEnabled = true` per vedere i box e i padding.\n- Il **Widget Inspector** di DevTools con la vista **Layout Explorer**: mostra vincoli, flex e dimensioni reali di ogni nodo.\n- `debugDumpRenderTree()` in console: stampa constraints e size di tutto l'albero di rendering, spesso è il modo più rapido per capire chi impone cosa.\n- I **golden test** sono l'alleato migliore per un layout custom: bloccano la resa visiva e ti avvisano se una modifica al delegate rompe un caso limite.\n\n## Conclusione\n\nI layout personalizzati non sono materia esclusiva per chi scrive framework: appena l'interfaccia esce dagli schemi, saper scendere di un livello evita workaround fragili basati su `MediaQuery` e numeri magici. Parti sempre dalla composizione, passa ai delegate quando la logica di posizionamento diventa il cuore del problema e riserva i RenderObject ai casi in cui devi controllare misura, paint e hit test. Il protocollo di layout di Flutter è semplice — vincoli giù, dimensioni su, posizione dal genitore — e una volta interiorizzato apre la porta a interfacce che sembrano impossibili con i soli widget standard.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F0742bec6-8ece-4da4-a49d-a863a910fe3f.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1760553120312-2821bf54e767?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODkwMTI5MTN8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"ANOOF C","https:\u002F\u002Funsplash.com\u002F@anoofc","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fminiature-industrial-complex-with-glowing-lights--VRT2sBQK9c",null,"published","2026-09-10T04:01:53+00:00","Layout personalizzati in Flutter: delegate e RenderObject","Guida pratica ai layout custom in Flutter: protocollo dei vincoli, CustomSingleChildLayout, CustomMultiChildLayout, Flow e RenderObject su misura.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789120577223]