Perché studiare gli InheritedWidget nel 2024
Ogni volta che scrivi Theme.of(context), MediaQuery.sizeOf(context) o Navigator.of(context) stai usando un InheritedWidget. Lo stesso vale per Provider, per lo ProviderScope di Riverpod e per il BlocProvider di flutter_bloc: sotto il cofano c'è sempre quel meccanismo.
Conoscerlo non è archeologia del framework: serve a capire perché un widget si ricostruisce, perché context a volte "non trova" quello che cerca, e come propagare dati senza aggiungere dipendenze esterne. In molte app un piccolo InheritedWidget scritto a mano è la soluzione più leggera e leggibile.
Il modello mentale: tre alberi, un solo lookup
Flutter mantiene tre alberi paralleli:
- Widget tree: la configurazione immutabile che descrivi nel
build. - Element tree: le istanze persistenti che collegano widget e render object. Il
BuildContextè unElement. - Render tree: ciò che viene effettivamente misurato e disegnato.
Quando chiami context.dependOnInheritedWidgetOfExactType<T>(), Flutter non scorre l'albero ogni volta: ogni Element mantiene una mappa (_inheritedElements) degli InheritedWidget disponibili sopra di lui, ereditata dal genitore. Il lookup è quindi un accesso a una hash map, O(1), non una risalita lineare. Per questo Theme.of(context) è economico anche in alberi profondi.
La parte costosa non è la lettura: è la dipendenza che viene registrata. Chi legge con dependOn... viene aggiunto alla lista dei dipendenti e sarà ricostruito ogni volta che l'InheritedWidget cambia (se updateShouldNotify restituisce true).
Un InheritedWidget scritto a mano
Ecco il pattern canonico, con la coppia of / maybeOf che ormai è convenzione nel framework:
class AppConfig extends InheritedWidget {
const AppConfig({
super.key,
required this.apiBaseUrl,
required this.isBetaUser,
required super.child,
});
final String apiBaseUrl;
final bool isBetaUser;
static AppConfig? maybeOf(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<AppConfig>();
static AppConfig of(BuildContext context) {
final config = maybeOf(context);
assert(config != null, 'Nessun AppConfig trovato sopra questo context');
return config!;
}
@override
bool updateShouldNotify(AppConfig oldWidget) =>
apiBaseUrl != oldWidget.apiBaseUrl || isBetaUser != oldWidget.isBetaUser;
}
Tre punti da non sottovalutare:
updateShouldNotifyè il tuo filtro anti-rebuild. Se restituisci sempretrue, ogni ricostruzione dell'antenato ricostruisce tutti i dipendenti. Confronta i campi che contano davvero.- I campi devono essere immutabili (
final). Se muti un oggetto interno senza sostituire il widget, il framework non ha modo di accorgersene. oflancia in assert,maybeOfrestituiscenull: dai al chiamante la possibilità di scegliere.
Stato mutabile: InheritedWidget + StatefulWidget
Un InheritedWidget è immutabile, quindi per gestire stato lo si accoppia a uno StatefulWidget che lo ricrea:
class CartScope extends StatefulWidget {
const CartScope({super.key, required this.child});
final Widget child;
static CartController of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<_CartInherited>()!.controller;
@override
State<CartScope> createState() => _CartScopeState();
}
class _CartScopeState extends State<CartScope> {
final CartController controller = CartController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) =>
_CartInherited(controller: controller, child: widget.child);
}
class _CartInherited extends InheritedWidget {
const _CartInherited({required this.controller, required super.child});
final CartController controller;
@override
bool updateShouldNotify(_CartInherited oldWidget) =>
controller != oldWidget.controller;
}
Qui updateShouldNotify confronta l'identità del controller: il controller in sé non cambia mai, quindi nessuno viene ricostruito inutilmente. La notifica dei cambiamenti la deleghi al controller stesso, che tipicamente è un ChangeNotifier o espone uno Stream.
InheritedNotifier: la scorciatoia per i ChangeNotifier
Se il tuo stato è un Listenable (ChangeNotifier, ValueNotifier, AnimationController), InheritedNotifier fa il lavoro sporco: si iscrive al notifier e ricostruisce i dipendenti a ogni notifyListeners().
class ThemeModeScope extends InheritedNotifier<ValueNotifier<ThemeMode>> {
const ThemeModeScope({
super.key,
required ValueNotifier<ThemeMode> super.notifier,
required super.child,
});
static ValueNotifier<ThemeMode> of(BuildContext context) => context
.dependOnInheritedWidgetOfExactType<ThemeModeScope>()!
.notifier!;
}
// Uso
final mode = ThemeModeScope.of(context).value;
// ...
ThemeModeScope.of(context).value = ThemeMode.dark;
Attenzione: la granularità è quella del notifier intero. Chi legge viene ricostruito anche se il campo che gli interessa non è cambiato. Per stati grandi, spezza in più scope o usa InheritedModel.
InheritedModel: dipendenze per "aspetto"
InheritedModel permette a un widget di dipendere solo da una fetta dei dati. Ogni dipendente dichiara l'aspetto che gli interessa e viene ricostruito solo se quell'aspetto cambia.
enum UserAspect { name, avatar }
class UserScope extends InheritedModel<UserAspect> {
const UserScope({
super.key,
required this.name,
required this.avatarUrl,
required super.child,
});
final String name;
final String avatarUrl;
static UserScope of(BuildContext context, UserAspect aspect) =>
InheritedModel.inheritFrom<UserScope>(context, aspect: aspect)!;
@override
bool updateShouldNotify(UserScope old) =>
name != old.name || avatarUrl != old.avatarUrl;
@override
bool updateShouldNotifyDependent(UserScope old, Set<UserAspect> aspects) {
if (aspects.contains(UserAspect.name) && name != old.name) return true;
if (aspects.contains(UserAspect.avatar) && avatarUrl != old.avatarUrl) {
return true;
}
return false;
}
}
Un widget che mostra solo l'avatar userà UserScope.of(context, UserAspect.avatar) e ignorerà i cambi di nome. È esattamente la tecnica adottata dal framework per MediaQuery: da Flutter 3.10 esistono MediaQuery.sizeOf(context), paddingOf, viewInsetsOf… proprio per evitare che un cambio di padding ricostruisca chi legge solo la dimensione.
Best practice immediata: nel codice nuovo preferisci
MediaQuery.sizeOf(context)aMediaQuery.of(context).size. È una riga che elimina rebuild inutili, per esempio all'apertura della tastiera.
dependOn vs getElementForInheritedWidgetOfExactType
Esistono due modi di leggere:
dependOnInheritedWidgetOfExactType<T>()— legge e registra la dipendenza. Da usare nelbuildo indidChangeDependencies.getInheritedWidgetOfExactType<T>()(ogetElementForInheritedWidgetOfExactType<T>()?.widget as T) — legge senza dipendere. Utile ininitState, indisposeo nei callback, quando ti serve solo un riferimento stabile (es. un controller) e non vuoi essere ricostruito.
@override
void initState() {
super.initState();
// OK: nessuna dipendenza registrata
final controller = context
.getInheritedWidgetOfExactType<_CartInherited>()!
.controller;
controller.load();
}
Chiamare dependOn... in initState genera un'eccezione, perché in quella fase l'element non può ancora registrare dipendenze in modo sicuro. Se ti serve reagire ai cambiamenti, il posto giusto è didChangeDependencies.
I tre errori più comuni con il BuildContext
1. Il context sbagliato
// SBAGLIATO: il context di build() è sopra lo Scaffold
Scaffold(
body: Builder(
builder: (innerContext) => ElevatedButton(
onPressed: () => Scaffold.of(innerContext).openDrawer(),
child: const Text('Apri'),
),
),
)
Un lookup vede solo gli antenati del proprio element. Se l'InheritedWidget è creato nello stesso build, serve un Builder (o un widget separato) per ottenere un context più in basso.
2. Leggere dopo un await
Future<void> _save(BuildContext context) async {
await repository.save();
if (!context.mounted) return; // indispensabile
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Salvato')),
);
}
Dal 2023 BuildContext.mounted è disponibile direttamente e il lint use_build_context_synchronously lo segnala: attivalo in analysis_options.yaml.
3. Dipendere da tutto per usare poco
Theme.of(context) in un widget che usa solo un colore va benissimo, ma MediaQuery.of(context) per la sola larghezza è uno spreco. Regola pratica: dipendi dal minimo indispensabile e il più in basso possibile nell'albero.
Quando usare un InheritedWidget e quando no
Usalo quando:
- devi passare una dipendenza a un sottoalbero senza attraversare dieci costruttori (prop drilling);
- stai scrivendo un package e non vuoi imporre una libreria di state management;
- il dato è di scope (tema, configurazione, sessione, controller di una schermata complessa).
Preferisci una soluzione dedicata (Riverpod, BLoC, Provider) quando ti servono: cache e disposal automatici, dipendenze fra provider, gestione di stati asincroni, testabilità con override. Quelle librerie non sostituiscono l'InheritedWidget: lo incapsulano aggiungendo ergonomia.
Verificare i rebuild
Per capire se le tue dipendenze sono troppo larghe, in DevTools attiva Track Widget Rebuilds (Flutter Inspector) oppure abilita temporaneamente:
import 'package:flutter/rendering.dart';
void main() {
debugPrintRebuildDirtyWidgets = true; // solo in debug
runApp(const MyApp());
}
Se vedi ricostruzioni a raffica all'apertura della tastiera o alla rotazione, quasi sempre la causa è un MediaQuery.of(context) posizionato troppo in alto nell'albero.
In sintesi
- Il
BuildContextè unElement: il lookup degli InheritedWidget è O(1) grazie a una mappa mantenuta da ogni element. updateShouldNotifydecide chi si ricostruisce: scrivilo con cura.InheritedNotifiersemplifica l'integrazione con iListenable;InheritedModeloffre dipendenze granulari per aspetto.- Usa
dependOn...nelbuild,get...ininitState/dispose. - Preferisci le API "of" granulari (
MediaQuery.sizeOf) e proteggi sempre il context dopo unawaitconcontext.mounted.
Padroneggiare questo livello del framework ti permette di scegliere consapevolmente lo strumento giusto — e di debuggare in cinque minuti problemi che altrimenti sembrano magia nera.