Flutter Internals: Understand the Framework for Writing Better Code
Flutter is built on three parallel and synchronized trees that most
of the developers he never knows directly, yet they determine every aspect
the performance and behavior of the app. Understanding the Widget trees,
theElement trees and the RenderObject tree transform the way
where you write Flutter code: explain why const and a win performance,
why setState and local, because InheritedWidget propagates data without rebuilding
useless and because GlobalKey is dangerous.
This article is not theoretical: each concept is accompanied by practical examples with real implications on performance and architecture. At the end you will be able to think about what it really happens when Flutter draws a frame.
What You Will Learn
- The three Flutter trees: Widget, Element and RenderObject
- Because Widgets are immutable and are rebuilt every frame
- How Element manages lifecycle and state
- The reconciliation process: how Flutter finds differences between frames
- Why
constwidget avoids rebuilding the Element tree - As
setStateonly marks the subtree that changes - InheritedWidget: Data propagation without cascading rebuilds
- Layout pass: constraints down, sizes up, positions during paint
- RepaintBoundary: isolate the repaint zones
The Three Trees of Flutter
When you write a Flutter widget, you are describing the UI declaratively. Flutter takes that description and builds three distinct data structures internally with different responsibilities. This separation is the foundation of Flutter's performance.
| Tree | Mutability | Responsibility | Duration |
|---|---|---|---|
| Widget trees | Immutable | UI configuration description | Short (rebuilt every build) |
| Element trees | Mutable | Lifecycle, state, reconciliation | Long (persists between rebuilds) |
| RenderObject tree | Mutable | Layout, hit testing, painting | Long (lazily updated) |
Widget Tree: The Immutable Description
A Widget in Flutter and a immutable configuration: describes how
a part of the UI should appear, but it does not contain state or render.
Every time you call build(), Flutter creates new Widget objects —
economical operation because Widgets are simply allocated configuration objects
on the heap and immediately candidates for garbage collection.
# Dimostrazione: Widget = configurazione immutabile
// Questo codice crea nuovi oggetti Widget ogni frame di build:
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
// build() restituisce un NUOVO oggetto Text ogni volta che viene chiamato
// Ma Flutter NON ricrea il RenderObject corrispondente ogni volta
// Controlla invece se la configurazione e "uguale" tramite il type + key
return Column(
children: [
// NUOVO oggetto Text creato ogni build, ma efficiente:
Text('Count: $_count'), // runtimeType: Text
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Increment'), // const: NON ricrea il Widget
),
],
);
}
}
// const Text('Increment') e allocato UNA SOLA VOLTA in memoria
// come costante compile-time. Ogni rebuild del parent usa lo stesso oggetto.
// Questo e il motivo per cui flutter lint raccomanda sempre const.
Element Tree: The Lifecycle Manager
The Element tree is the heart of the Flutter framework. Each node of the Element tree matches a Widget in the tree, but persists across rebuilds. The Element knows the current Widget, manages the state (via StatefulElement), and decides whether to update the RenderObject or create a new one.
# Il processo di reconciliation (diffing) di Flutter
// Scenario: aggiorno il type di un widget in una lista
// PRIMA del rebuild:
Column(
children: [
Text('Hello'), // Element: TextElement
Icon(Icons.star), // Element: LeafRenderObjectElement
],
)
// DOPO il rebuild:
Column(
children: [
Text('Hello'), // stesso type -> Flutter RIUSA l'Element
ElevatedButton( // type diverso -> Flutter DISTRUGGE vecchio Element
onPressed: () {}, // e CREA nuovo Element (e RenderObject)
child: const Text('OK'),
),
],
)
// La regola: Flutter fa il match degli Element per POSIZIONE e TIPO (+ Key se presente)
// Se type corrisponde: aggiorna la configurazione dell'Element esistente
// Se type non corrisponde: distrugge il vecchio Element e crea tutto da zero
// IMPLICAZIONE: le liste dinamiche DEVONO usare Key
// SENZA Key (SBAGLIATO):
ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(items[index].name), // no key!
),
)
// Se riordini la lista, Flutter puo assegnare lo stato sbagliato agli item
// CON Key (CORRETTO):
ListView.builder(
itemBuilder: (context, index) => ListTile(
key: ValueKey(items[index].id), // chiave unica e stabile
title: Text(items[index].name),
),
)
StatefulWidget: Where the State Lives
One of the most frequently asked questions from new Flutter developers is: "why the state
isn't it lost when the Widget is rebuilt?". The answer is in the Element tree:
the state lives in the Element (specifically in StatefulElement), not
in the Widget.
# Perche lo stato persiste tra rebuild
// StatefulWidget: Widget (immutabile) + State (mutabile)
class MyWidget extends StatefulWidget {
const MyWidget({super.key, required this.title});
final String title; // configurazione immutabile
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// LO STATO VIVE QUI, nell'oggetto State
// L'Element tiene un riferimento a questo State
int _counter = 0;
@override
Widget build(BuildContext context) {
// Il Widget (MyWidget) viene ricreato, ma _MyWidgetState persiste
// L'Element aggiorna il suo _widget (la nuova configurazione)
// ma mantiene il suo _state (il vecchio State)
return Text('${widget.title}: $_counter');
// widget.title viene aggiornato automaticamente dall'Element
// quando il parent ricostruisce con un nuovo title
}
}
// Lifecycle completo di StatefulElement:
// 1. Element.mount() -> State.initState()
// 2. Element.update() -> State.didUpdateWidget() (se widget parent ricostruisce)
// 3. Element.deactivate() -> State.deactivate() (rimosso temporaneamente dal tree)
// 4. Element.unmount() -> State.dispose() (rimosso definitivamente)
// IMPORTANTE: dispose() DEVE liberare tutte le risorse
// (AnimationController, StreamSubscription, TextEditingController, etc.)
@override
void dispose() {
_controller.dispose(); // AnimationController
_subscription.cancel(); // StreamSubscription
_textController.dispose(); // TextEditingController
super.dispose();
}
RenderObject Tree: Layout and Painting
The RenderObject tree is the closest level to the metal: it manages the layout (calculation of positions and sizes), hit testing (which widget responds to touch) and painting (drawing on Canvas). Compared to the Element tree, the RenderObject tree it is updated only when necessary, and the updates are incremental.
# Il Layout pass: constraints down, sizes up, positions during paint
// Flutter usa un sistema di constraint-based layout:
// 1. Il parent passa CONSTRAINTS al child (max/min width/height)
// 2. Il child calcola la sua SIZE all'interno dei constraints
// 3. Il parent posiziona il child durante il paint
// Esempio: come Column calcola il layout
Column(
children: [
// 1. Column passa: BoxConstraints(maxWidth: 390, maxHeight: infinity)
Text('Hello'),
// Text risponde: "ho bisogno di 40px height"
// 2. Column passa constraints rimanenti al secondo child
Expanded(
// Expanded usa TUTTA l'altezza rimanente
child: ListView(...),
),
],
)
// RepaintBoundary: isola una zona dal ciclo di repaint
// Usalo per widget che si aggiornano spesso e non devono
// far ridisegnare il resto della UI
RepaintBoundary(
child: AnimatedProgressBar(progress: _progress),
)
// Senza RepaintBoundary: ogni frame del progressbar ridisegna tutta la pagina
// Con RepaintBoundary: solo il progressbar viene ridisegnato ogni frame
// CustomPainter con shouldRepaint ottimizzato:
class ChartPainter extends CustomPainter {
const ChartPainter({required this.data, required this.color});
final List<double> data;
final Color color;
@override
void paint(Canvas canvas, Size size) {
// ... codice di disegno
}
@override
bool shouldRepaint(ChartPainter oldDelegate) {
// Ridisegna SOLO se i dati o il colore sono cambiati
// Confronto reference per le liste (usa listEquals per confronto profondo)
return oldDelegate.data != data || oldDelegate.color != color;
}
}
const Widget: The Misunderstood Performance Win
The keyword const on a widget is not just a stylistic preference:
and a fundamental optimization that eliminates reconciliation work in the Element tree.
# const: perche e una performance win concreta
// SENZA const: ogni build() del parent crea nuovi oggetti
class ParentWidget extends StatefulWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// NUOVO oggetto Text creato ogni rebuild del parent
// Flutter deve fare il confronto nell'Element tree
Text('Static label'),
// widget che cambia davvero
Text('Dynamic: $_counter'),
],
);
}
}
// CON const: Flutter usa lo stesso oggetto compile-time
class ParentWidget extends StatefulWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Stesso oggetto compile-time, NESSUN confronto nell'Element tree
const Text('Static label'),
// Solo questo widget causa lavoro all'Element tree
Text('Dynamic: $_counter'),
],
);
}
}
// La regola: const rende il Widget una "costante compile-time"
// L'Element tree vede che il Widget e identico (stesso riferimento in memoria)
// e salta completamente la fase di reconciliation per quel sottoalbero
// flutter lint: "prefer_const_constructors" ti dice dove aggiungere const
// flutter analyze conta quante opportunita stai perdendo
InheritedWidget: Efficient Data Propagation
InheritedWidget is the mechanism by which Flutter propagates data down the tree without having to pass parameters through each layer. And the basis of Theme, MediaQuery, Navigator and Riverpod itself.
# InheritedWidget: come funziona internamente
// Implementazione manuale (come funziona Theme, MediaQuery, etc.)
class AppConfig extends InheritedWidget {
const AppConfig({
super.key,
required this.apiBaseUrl,
required this.isDarkMode,
required super.child,
});
final String apiBaseUrl;
final bool isDarkMode;
// Metodo statico per accedere dal tree discendente
static AppConfig of(BuildContext context) {
final config = context.dependOnInheritedWidgetOfExactType<AppConfig>();
assert(config != null, 'AppConfig non trovato nel tree');
return config!;
}
// CRITICO: updateShouldNotify decide quali widget discendenti si ricostruiscono
// Restituisci TRUE solo se il dato e davvero cambiato
@override
bool updateShouldNotify(AppConfig oldWidget) {
return apiBaseUrl != oldWidget.apiBaseUrl ||
isDarkMode != oldWidget.isDarkMode;
}
}
// Utilizzo: qualsiasi widget discendente puo accedere ad AppConfig
class SomeDeepWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// context.dependOnInheritedWidgetOfExactType registra questo widget
// come "dipendente" da AppConfig: si ricostruira quando AppConfig cambia
final config = AppConfig.of(context);
return Text(config.isDarkMode ? 'Dark Mode' : 'Light Mode');
}
}
// DIFFERENZA IMPORTANTE:
// context.dependOnInheritedWidgetOfExactType -> si iscrive agli update (rebuild)
// context.getInheritedWidgetOfExactType -> legge senza iscriversi (no rebuild)
// Questa distinzione e il motivo per cui:
// Theme.of(context) -> causa rebuild quando il tema cambia
// Theme.of(context).colors -> stesso effetto, non filtra per sub-proprieta
setState: Local Scope and Why is Efficient
# setState: marca solo il sottoalbero necessario
// setState non "ricostruisce tutta l'app" - marca solo l'Element di QUESTO State
// come "dirty" nella lista dei widget da rebuildarsi nel prossimo frame
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
// AppBar non dipende da _count -> NON si ricostruisce
appBar: const AppBar(title: Text('Counter')),
body: Center(
// SOLO questo Text si ricostruisce (il suo Element viene marcato dirty)
child: Text('$_count'),
),
floatingActionButton: FloatingActionButton(
// La FAB non dipende da _count -> NON si ricostruisce
onPressed: () => setState(() => _count++),
child: const Icon(Icons.add),
),
);
}
}
// Per ridurre ulteriormente il scope del rebuild: estrai il widget che cambia
// in un StatefulWidget separato con il suo setState locale
// ANTI-PATTERN: setState al livello piu alto per dati condivisi
// SOLUZIONE: Riverpod, InheritedWidget o altri state management
// che permettono rebuild granulari solo ai widget che usano quel dato
Keys: Guide to Types and When to Use Them
# Keys: ValueKey, ObjectKey, UniqueKey, GlobalKey
// ValueKey: key basata su un valore primitivo (id, stringa)
// USO: liste ordinate/filtrate dove gli item possono spostarsi
ListView.builder(
itemBuilder: (context, index) => ProductCard(
key: ValueKey(products[index].id), // usa l'id del dominio, stabile
product: products[index],
),
)
// ObjectKey: key basata su un oggetto (confronto per identita)
// USO: quando hai un oggetto come chiave unica
ValueKey(product.id) // preferibile: confronta l'ID (stringa/int)
ObjectKey(product) // confronta il riferimento all'oggetto
// UniqueKey: genera una key unica ogni volta (non stabile tra rebuild!)
// USO: forzare la ricostruzione di un widget (reset del suo stato)
UniqueKey() // ATTENZIONE: ogni rebuild crea una key diversa = State perso
// GlobalKey: accede allo State o al RenderObject da fuori del tree
// RARO: evita quando possibile, ha overhead significativo
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(children: [...]),
)
// Poi: _formKey.currentState!.validate()
// REGOLA: usa GlobalKey solo per Form, ScaffoldMessenger, Navigator
// Per tutto il resto: passa callbacks o usa state management
Performance Checklist: Flutter Internals in Practice
-
const wherever possible: every widget must be static
const. Enable the lint ruleprefer_const_constructors. -
Key in dynamic lists: any
ListView.builderoColumnwith dynamic children must useValueKeybased on the domain ID. - setState at the lowest level: extract into separate StatefulWidget the part of UI that changes instead of calling setState at the root level.
-
RepaintBoundary for animations: wrap continuous animations
(loading spinner, progress bar, countdown) in
RepaintBoundary. -
shouldRepaint in CustomPainter: always implement logic
correct instead of always returning
true. - No unnecessary GlobalKeys: each GlobalKey has overhead. Use it only when strictly necessary (Form, Scaffold, Navigator).
Conclusions
Understanding Flutter's three trees — Widget, Element, and RenderObject — transforms
every code decision from "vague best practice" to causal understanding: you know
exactly why const it works, because setState e
efficient, because InheritedWidget does not cause unnecessary cascade rebuilds.
This internal knowledge is not an academic detail: it is what separates a Flutter developer who "makes things work" from someone who builds apps with 60fps guaranteed, predictable memory footprint and architecture that remains performing as the codebase grows. The framework has made precise architectural choices — understanding them means being able to collaborate with the framework instead of working against it.







