[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"articolo-memory-leak-in-flutter-individuarli-e-prevenirli-con-devtools-e-leak-tracker":3,"comments-article-memory-leak-in-flutter-individuarli-e-prevenirli-con-devtools-e-leak-tracker":27},{"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},106,"Memory leak in Flutter: individuarli e prevenirli con DevTools e leak_tracker","memory-leak-in-flutter-individuarli-e-prevenirli-con-devtools-e-leak-tracker","Anche in Dart, con il garbage collector, le app possono consumare memoria senza motivo. Vediamo le cause più comuni dei memory leak in Flutter, come individuarli con DevTools e come prevenirli automaticamente con leak_tracker nei test.","## Perché un'app Flutter può \"perdere\" memoria\n\nDart ha un garbage collector, quindi molti sviluppatori danno per scontato che i memory leak non siano un problema. In realtà il GC libera solo gli oggetti **non più raggiungibili**: se un `Timer`, uno `StreamSubscription` o un listener globale mantiene un riferimento a uno `State` già smontato, quell'oggetto (e con lui l'intero sottoalbero di elementi, controller e immagini a cui punta) resta vivo per sempre.\n\nI sintomi tipici sono:\n\n- la memoria cresce ad ogni navigazione avanti\u002Findietro tra due schermate;\n- dopo qualche minuto d'uso l'app diventa scattosa, soprattutto su dispositivi Android di fascia bassa;\n- crash `OutOfMemory` o terminazioni improvvise su iOS;\n- eccezioni del tipo `A ChangeNotifier was used after being disposed` o `setState() called after dispose()`.\n\nIn questo articolo vediamo le cause ricorrenti, come individuarle con DevTools e come trasformare la caccia al leak in un controllo automatico nei test grazie a **leak_tracker**, ormai integrato in `flutter_test`.\n\n## Le cause più comuni\n\n### 1. Controller e notifier non chiusi\n\nÈ il caso più frequente. `TextEditingController`, `AnimationController`, `ScrollController`, `ChangeNotifier`, `ValueNotifier`, `FocusNode`, `TabController`: tutti questi oggetti registrano listener e vanno chiusi esplicitamente.\n\n```dart\nclass _SearchFieldState extends State\u003CSearchField> {\n  late final TextEditingController _controller;\n  late final FocusNode _focusNode;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = TextEditingController();\n    _focusNode = FocusNode();\n  }\n\n  @override\n  void dispose() {\n    \u002F\u002F Ordine inverso rispetto alla creazione: prima i dipendenti.\n    _controller.dispose();\n    _focusNode.dispose();\n    super.dispose();\n  }\n}\n```\n\nAttenzione: se passi un controller creato dal widget padre, **non** deve essere il figlio a chiuderlo. La regola è semplice: *chi crea l'oggetto lo distrugge*.\n\n### 2. Sottoscrizioni a stream e timer dimenticati\n\n```dart\nclass _PositionViewState extends State\u003CPositionView> {\n  StreamSubscription\u003CPosition>? _sub;\n  Timer? _timer;\n\n  @override\n  void initState() {\n    super.initState();\n    _sub = positionStream.listen(_onPosition);\n    _timer = Timer.periodic(const Duration(seconds: 5), (_) => _refresh());\n  }\n\n  @override\n  void dispose() {\n    _sub?.cancel();\n    _timer?.cancel();\n    super.dispose();\n  }\n}\n```\n\nUn `Timer.periodic` non cancellato è un leak doppio: consuma CPU e tiene in vita la closure, quindi lo `State`, quindi l'intero albero.\n\n### 3. Listener registrati su oggetti a vita lunga\n\nQuesto è il caso più insidioso, perché il riferimento parte da un singleton e arriva al widget.\n\n```dart\n\u002F\u002F ❌ Il servizio è un singleton: il listener sopravvive alla pagina.\nSessionService.instance.addListener(_onSessionChanged);\n\n\u002F\u002F ✅ Rimuovilo sempre\n@override\nvoid dispose() {\n  SessionService.instance.removeListener(_onSessionChanged);\n  super.dispose();\n}\n```\n\nLo stesso vale per `WidgetsBinding.instance.addObserver(this)`, per i callback registrati su bus di eventi, per `AppLifecycleListener` e per i listener di package nativi (connettività, notifiche, Bluetooth).\n\n### 4. Cache statiche senza limiti\n\nUna `static final Map\u003CString, Uint8List> _cache = {}` riempita con anteprime di immagini è un leak per definizione: nessuno la svuota mai. Se ti serve una cache, usa una struttura con capacità massima (LRU) oppure appoggiati a `ImageCache`, che Flutter gestisce già con limiti configurabili:\n\n```dart\nPaintingBinding.instance.imageCache\n  ..maximumSize = 200            \u002F\u002F numero di immagini\n  ..maximumSizeBytes = 80 \u003C\u003C 20; \u002F\u002F 80 MB\n```\n\n### 5. Closure che catturano il `BuildContext`\n\nPassare un `BuildContext` a un oggetto che vive più a lungo del widget (un service locator, un client HTTP, un singleton di navigazione) tiene in vita l'`Element` e tutto il suo sottoalbero. Passa invece i dati già estratti, o usa una `GlobalKey\u003CNavigatorState>` dedicata.\n\n## Individuare un leak con DevTools\n\nLa procedura più affidabile è il **diff tra snapshot**, perché isola esattamente gli oggetti creati e mai liberati durante un ciclo di navigazione.\n\n1. Avvia l'app in modalità **profile** (`flutter run --profile`): in debug i numeri sono falsati dal codice di assert e dal JIT.\n2. Apri DevTools e vai nella tab **Memory**.\n3. Porta l'app nella schermata di partenza (per esempio la home) e premi **Take heap snapshot**.\n4. Entra nella schermata sospetta e torna indietro. Ripeti il ciclo 5–10 volte.\n5. Premi il pulsante **GC** per forzare una garbage collection, poi scatta un secondo snapshot.\n6. Apri la sezione **Diff** e confronta i due snapshot.\n\nSe nella colonna delle differenze vedi 5 o 10 istanze in più di `_DetailPageState`, `AnimationController` o `Image`, hai trovato il leak. Selezionando la classe e poi una singola istanza, DevTools mostra il **retaining path**: la catena di riferimenti che impedisce al GC di liberare l'oggetto. È lì che troverai il listener o il timer colpevole.\n\nAltri strumenti utili nella stessa pagina:\n\n- **Trace instances**: attiva il tracciamento delle allocazioni per classi specifiche e mostra lo stack trace di creazione.\n- **Chart**: il grafico di `Dart Heap` e `RSS`. Se la linea di base dopo ogni GC sale a gradini invece di tornare al valore iniziale, c'è una crescita non fisiologica.\n\nUn dettaglio importante su Flutter: la memoria delle immagini decodificate non è nel Dart heap ma nella memoria nativa (RSS). Se l'RSS cresce mentre il Dart heap è stabile, il sospetto principale è la cache delle immagini o un `ui.Image` creato manualmente e mai `dispose()`-ato.\n\n## Automatizzare: leak_tracker nei test\n\nIndividuare un leak a mano è utile una volta; evitarlo per sempre è meglio. Flutter integra **leak_tracker**, che verifica durante i widget test che ogni oggetto \"disposable\" creato venga effettivamente disposto e poi raccolto dal GC.\n\nAggiungi la dipendenza:\n\n```yaml\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  leak_tracker_flutter_testing: ^3.0.0\n```\n\nPuoi abilitare il tracciamento su tutta la suite con un file di configurazione `test\u002Fflutter_test_config.dart`:\n\n```dart\nimport 'dart:async';\nimport 'package:leak_tracker_flutter_testing\u002Fleak_tracker_flutter_testing.dart';\n\nFuture\u003Cvoid> testExecutable(FutureOr\u003Cvoid> Function() testMain) async {\n  LeakTesting.enable();\n  LeakTesting.settings = LeakTesting.settings.withTrackedAll();\n  await testMain();\n}\n```\n\nOppure attivarlo test per test, utile quando introduci il controllo in un progetto esistente:\n\n```dart\ntestWidgets(\n  'la pagina dettaglio non perde memoria',\n  experimentalLeakTesting: LeakTesting.settings.withTrackedAll(),\n  (tester) async {\n    await tester.pumpWidget(const MaterialApp(home: DetailPage(id: 1)));\n    await tester.pumpAndSettle();\n\n    \u002F\u002F Smonta la pagina: tutti i controller devono essere disposti.\n    await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));\n    await tester.pumpAndSettle();\n  },\n);\n```\n\nSe un `AnimationController` non viene chiuso, il test fallisce con un messaggio del tipo `not disposed` e lo stack trace del punto di creazione: informazione preziosa, molto più del generico \"l'app consuma memoria\".\n\nPer i falsi positivi provenienti da package di terze parti puoi escludere classi specifiche:\n\n```dart\nLeakTesting.settings = LeakTesting.settings\n    .withTrackedAll()\n    .withIgnored(classes: const ['SomeThirdPartyController']);\n```\n\n## Rendere i propri oggetti tracciabili\n\nSe scrivi un package o una classe che gestisce risorse, puoi notificare al framework la creazione e la distruzione dell'oggetto, così leak_tracker e DevTools lo includono nei report:\n\n```dart\nimport 'package:flutter\u002Ffoundation.dart';\n\nclass AudioSessionController {\n  AudioSessionController() {\n    if (kFlutterMemoryAllocationsEnabled) {\n      FlutterMemoryAllocations.instance.dispatchObjectCreated(\n        library: 'my_package',\n        className: '$AudioSessionController',\n        object: this,\n      );\n    }\n  }\n\n  void dispose() {\n    if (kFlutterMemoryAllocationsEnabled) {\n      FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);\n    }\n    \u002F\u002F ... rilascio delle risorse\n  }\n}\n```\n\n## Pattern difensivi che riducono i leak\n\n### Un mixin per non dimenticare il `dispose`\n\n```dart\nmixin DisposableBag\u003CT extends StatefulWidget> on State\u003CT> {\n  final List\u003CVoidCallback> _disposers = [];\n\n  void autoDispose(VoidCallback disposer) => _disposers.add(disposer);\n\n  @override\n  void dispose() {\n    for (final d in _disposers.reversed) {\n      d();\n    }\n    _disposers.clear();\n    super.dispose();\n  }\n}\n```\n\nUtilizzo:\n\n```dart\n@override\nvoid initState() {\n  super.initState();\n  final sub = repository.watchItems().listen(_onItems);\n  autoDispose(sub.cancel);\n\n  final controller = ScrollController();\n  autoDispose(controller.dispose);\n}\n```\n\n### Preferire i widget che gestiscono il ciclo di vita\n\n`StreamBuilder`, `ValueListenableBuilder`, `AnimatedBuilder` e `ListenableBuilder` si iscrivono e si disiscrivono da soli: usarli al posto di `addListener` manuali elimina un'intera categoria di errori. Allo stesso modo, i provider di Riverpod o i `Bloc` creati tramite `BlocProvider` vengono chiusi automaticamente quando escono di scope.\n\n### Attenzione ai `keepAlive` e agli oggetti globali\n\nUn provider marcato `keepAlive` o un singleton registrato in `get_it` non sono leak di per sé, ma diventano radici permanenti: qualsiasi cosa a cui puntano resta viva. Verifica che non conservino liste di risultati illimitate o riferimenti a schermate.\n\n### Lint che aiutano\n\nNel file `analysis_options.yaml`:\n\n```yaml\nlinter:\n  rules:\n    - close_sinks\n    - cancel_subscriptions\n    - use_build_context_synchronously\n```\n\nNon coprono tutti i casi (l'analizzatore non traccia i riferimenti a runtime), ma intercettano gli errori più banali già in fase di scrittura.\n\n## Un flusso di lavoro consigliato\n\n1. **Prevenzione**: attiva i lint e usa widget che gestiscono da soli le sottoscrizioni.\n2. **Protezione**: abilita leak_tracker sui widget test delle schermate principali; diventa una rete di sicurezza contro le regressioni.\n3. **Diagnosi**: quando il consumo cresce in produzione, riproduci lo scenario in profile mode e usa il diff degli snapshot di DevTools per trovare il retaining path.\n4. **Verifica**: dopo la correzione, ripeti il ciclo di 10 navigazioni e controlla che il numero di istanze torni stabile.\n\nI memory leak non sono un problema esotico riservato ai linguaggi senza GC: in Flutter nascono quasi sempre da una riga dimenticata nel `dispose`. La buona notizia è che gli strumenti per scovarli sono già nel framework e, con leak_tracker nei test, si può smettere di cercarli a mano.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fc065ba90-53cc-4f14-a948-f0b74e6f2037.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1742072593996-ebdc5d605a54?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAxMzYwODV8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Peaky Frames","https:\u002F\u002Funsplash.com\u002F@rashedpaykary","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fcolorful-code-scrolls-across-a-dark-background-Aowg76xooEY",null,"published","2026-09-23T04:01:26+00:00","Memory leak in Flutter: DevTools e leak_tracker","Come individuare e prevenire i memory leak in Flutter: cause comuni, diff degli heap snapshot in DevTools e verifica automatica con leak_tracker nei test.",{"id":21,"name":22,"slug":23},3,"Best practice","best-practice",{"id":25,"name":26},1,"Flutter Bot",[],1792316909486]