[{"data":1,"prerenderedAt":86},["ShallowReactive",2],{"tutorial-scovare-e-prevenire-i-memory-leak-in-flutter-con-devtools-e-leak-tracker":3,"comments-tutorial-scovare-e-prevenire-i-memory-leak-in-flutter-con-devtools-e-leak-tracker":85},{"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":34},112,"Scovare e prevenire i memory leak in Flutter con DevTools e leak_tracker","scovare-e-prevenire-i-memory-leak-in-flutter-con-devtools-e-leak-tracker","Guida avanzata per diagnosticare perdite di memoria in Flutter: snapshot e retaining path in DevTools, test automatici con leak_tracker, pattern di dispose sicuri e controlli in CI.","Un'app Flutter può sembrare perfetta in debug e poi degradare dopo venti minuti d'uso: frame rate che cala, consumo di RAM che cresce a ogni navigazione, crash OOM sui dispositivi Android di fascia media. Nella quasi totalità dei casi la causa non è il framework, ma **oggetti che restano referenziati più a lungo del necessario**: `AnimationController` non dismessi, `StreamSubscription` mai cancellate, listener registrati su `ChangeNotifier` globali, closure che catturano un `BuildContext` o un intero `State`.\n\nIn Dart non esiste un `free()`: il garbage collector libera solo ciò che non è più raggiungibile. Un *leak* in Flutter è quindi sempre un **problema di grafo di riferimenti**. La buona notizia è che oggi abbiamo strumenti di prima classe per vederlo: la vista *Memory* di DevTools con diff degli snapshot e *retaining path*, e il pacchetto `leak_tracker` che Flutter usa internamente e che possiamo attivare nei nostri widget test per far **fallire la build** quando un oggetto non viene dismesso.\n\nIn questo tutorial costruiremo un caso di leak realistico, lo isoleremo con DevTools, lo automatizzeremo in test, lo correggeremo con pattern riutilizzabili e infine renderemo il controllo parte della pipeline CI. Serve dimestichezza con `StatefulWidget`, stream e ciclo di vita dei widget.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F69e8c38d-94b7-4d2b-a0db-21c5a1b334fb.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1741392075937-bb51b543aa18?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA5MTU1MTl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Brecht Corbeel","https:\u002F\u002Funsplash.com\u002F@brechtcorbeel","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-gigabyte-graphics-card-installed-in-a-computer-bmdqtp7RvTo","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=7BifjsRvclM",{"youtube_id":17,"duration_seconds":18,"published_at":19},"7BifjsRvclM",191,"2026-10-02T10:52:56+00:00","advanced",55,"3.x (3.19+ consigliato)","published","2026-10-02T04:31:59+00:00","Memory leak in Flutter: DevTools e leak_tracker","Tutorial avanzato: diagnosticare memory leak in Flutter con DevTools, automatizzare i controlli con leak_tracker e correggere il dispose dei widget.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,51,57,64,71,78],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},749,"Capire cosa è davvero un leak in Flutter e preparare il progetto","Prima di aprire DevTools serve un modello mentale preciso. In Flutter distinguiamo tre situazioni:\n\n1. **Not disposed**: un oggetto `Disposable` (es. `AnimationController`, `TextEditingController`, `FocusNode`, `ScrollController`) viene raccolto dal GC senza che sia stato chiamato `dispose()`. Non è sempre un leak di memoria, ma è quasi sempre un leak di *risorse* (ticker attivi, listener nativi).\n2. **Not GCed \u002F late GC**: `dispose()` è stato chiamato, ma l'oggetto resta raggiungibile da una radice (static, singleton, listener non rimosso) e quindi non viene mai liberato. Questo è il leak classico che fa crescere la RAM.\n3. **Crescita fisiologica**: cache (`ImageCache`, cache di rete) che crescono fino a un limite. Non sono leak, ma vanno configurate.\n\nIl bersaglio tipico è l'oggetto `State`: se un leak trattiene un `State`, trattiene l'intero sottoalbero di element e render object, quindi anche immagini decodificate. Un singolo leak su una schermata di dettaglio può costare decine di MB.\n\nAggiungi le dipendenze di sviluppo per il tracciamento automatico (le useremo dal passo 4) e assicurati di poter lanciare l'app in **profile mode**: in debug i numeri della memoria sono inquinati dalle asserzioni e dal codice non ottimizzato.","# pubspec.yaml\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  # tracciamento leak nei widget test\n  leak_tracker: ^10.0.0\n  leak_tracker_flutter_testing: ^3.0.0\n\n# Verifica l'allineamento delle versioni con il canale Flutter in uso:\n#   flutter pub get\n#   flutter pub deps | grep leak_tracker\n\n# Esecuzione in profile mode per il profiling di memoria:\n#   flutter run --profile --track-widget-creation","bash","Il progetto compila con le dipendenze di leak tracking e l'app parte in profile mode mostrando l'URL del DevTools nella console.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":49,"expected_result":50,"demo_url":42,"video_url":42},750,2,"Riprodurre un leak deterministico e misurabile","Per imparare a leggere gli strumenti serve un leak *controllato*. Creiamo una schermata di dettaglio che commette tre errori molto comuni nel mondo reale:\n\n- si iscrive a uno `Stream` globale senza cancellare la `StreamSubscription`;\n- registra un listener su un singleton (`AppBus`) senza rimuoverlo in `dispose()`;\n- memorizza se stessa (il `State`) in una mappa statica usata come \"cache di comodo\".\n\nOgni volta che apri e chiudi la schermata, il `State` resta vivo e con esso l'albero di widget, i controller e le immagini decodificate. È esattamente lo scenario che produce la crescita lineare di RAM a ogni push\u002Fpop.\n\nAttenzione al punto più subdolo: anche il solo `_bus.addListener(_onEvent)` basta. `_onEvent` è un *tear-off* di un metodo di istanza, quindi la closure cattura `this`, cioè il `State`. Il singleton, che vive per tutta la durata dell'app, diventa una radice GC che trattiene la schermata.","import 'dart:async';\nimport 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Singleton che vive per tutta l'app: ottima radice per un leak.\nclass AppBus extends ChangeNotifier {\n  AppBus._();\n  static final AppBus instance = AppBus._();\n\n  final _events = StreamController\u003CString>.broadcast();\n  Stream\u003CString> get events => _events.stream;\n\n  void emit(String e) {\n    _events.add(e);\n    notifyListeners();\n  }\n}\n\n\u002F\u002F\u002F \"Cache\" statica: altro classico anti-pattern.\nfinal Map\u003Cint, Object> leakyCache = \u003Cint, Object>{};\n\nclass LeakyDetailPage extends StatefulWidget {\n  const LeakyDetailPage({super.key, required this.id});\n  final int id;\n\n  @override\n  State\u003CLeakyDetailPage> createState() => _LeakyDetailPageState();\n}\n\nclass _LeakyDetailPageState extends State\u003CLeakyDetailPage>\n    with SingleTickerProviderStateMixin {\n  late final AnimationController _controller;\n  StreamSubscription\u003CString>? _sub;\n  String _last = '-';\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = AnimationController(\n      vsync: this,\n      duration: const Duration(seconds: 1),\n    )..repeat();\n\n    \u002F\u002F LEAK 1: subscription mai cancellata.\n    _sub = AppBus.instance.events.listen((e) {\n      if (mounted) setState(() => _last = e);\n    });\n\n    \u002F\u002F LEAK 2: listener su singleton mai rimosso (cattura `this`).\n    AppBus.instance.addListener(_onBusChanged);\n\n    \u002F\u002F LEAK 3: lo State finisce in una mappa statica.\n    leakyCache[widget.id] = this;\n  }\n\n  void _onBusChanged() {\u002F* ... *\u002F}\n\n  @override\n  void dispose() {\n    \u002F\u002F Nota: manca tutto il cleanup, e persino _controller.dispose().\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Dettaglio ${widget.id}')),\n      body: Center(\n        child: RotationTransition(\n          turns: _controller,\n          child: Text('Ultimo evento: $_last'),\n        ),\n      ),\n    );\n  }\n}","dart","Aprendo e chiudendo più volte la schermata l'app continua a funzionare, ma il consumo di memoria cresce in modo monotono ad ogni ciclo.",{"id":52,"position":28,"title":53,"body":54,"code_snippet":55,"code_language":49,"expected_result":56,"demo_url":42,"video_url":42},751,"Isolare il leak con DevTools: snapshot, diff e retaining path","Ora la parte investigativa. Avvia l'app in **profile mode** e apri DevTools sulla scheda *Memory*.\n\nProcedura rigorosa (il \"protocollo dei tre snapshot\"):\n\n1. Porta l'app in uno stato stabile (home screen) e premi **GC** manuale, poi **Take heap snapshot**: è la tua baseline.\n2. Esegui **N volte identiche** l'azione sospetta (es. 10 push + pop della schermata di dettaglio). Più alto è N, più il segnale emerge dal rumore.\n3. Torna alla home, premi di nuovo **GC** e scatta il secondo snapshot. Usa il **Diff** tra i due snapshot.\n\nNella tabella di diff ordina per *instances delta*: se vedi `_LeakyDetailPageState` con delta +10, il verdetto è immediato. Seleziona la classe e apri il pannello **Retaining path**: DevTools mostra la catena di riferimenti dalla radice GC all'oggetto, ad esempio `AppBus (static) → _listeners → closure → _LeakyDetailPageState`. Quella catena *è* il bug: il fix consiste nel tagliarne un anello.\n\nSuggerimenti operativi:\n\n- Attiva **Allocation Tracing** sulla classe sospetta per avere lo stack trace del punto di allocazione.\n- Guarda anche la sezione *Dart \u002F RSS* del grafico: se il Dart heap è stabile ma la RSS cresce, il sospetto si sposta su memoria nativa (immagini decodificate, plugin, texture).\n- Puoi loggare gli eventi di creazione\u002Fdispose dal codice agganciandoti a `FlutterMemoryAllocations`, utile quando non puoi tenere DevTools aperto (es. su dispositivo di un tester).","import 'package:flutter\u002Ffoundation.dart';\nimport 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Conta creazioni e dispose degli oggetti \"disposable\" del framework.\n\u002F\u002F\u002F Da attivare SOLO in profile\u002Fdebug: ha un costo non trascurabile.\nvoid installAllocationProbe() {\n  if (kReleaseMode) return;\n\n  final alive = \u003CString, int>{};\n\n  FlutterMemoryAllocations.instance.addListener((event) {\n    if (event is ObjectCreated) {\n      alive.update(event.object.runtimeType.toString(), (v) => v + 1,\n          ifAbsent: () => 1);\n    } else if (event is ObjectDisposed) {\n      alive.update(event.object.runtimeType.toString(), (v) => v - 1,\n          ifAbsent: () => -1);\n    }\n  });\n\n  \u002F\u002F Stampa periodica del bilancio creati\u002Fdismessi.\n  Stream\u003Cvoid>.periodic(const Duration(seconds: 10)).listen((_) {\n    final sospetti = alive.entries.where((e) => e.value > 0).toList()\n      ..sort((a, b) => b.value.compareTo(a.value));\n    debugPrint('--- Oggetti vivi (creati - dismessi) ---');\n    for (final e in sospetti.take(10)) {\n      debugPrint('${e.key}: ${e.value}');\n    }\n  });\n}\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  installAllocationProbe();\n  runApp(const MyApp());\n}","Nel diff degli snapshot compaiono N istanze residue di _LeakyDetailPageState e il retaining path indica chiaramente il singleton AppBus come radice che le trattiene.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":49,"expected_result":63,"demo_url":42,"video_url":42},752,4,"Automatizzare la rilevazione con leak_tracker nei widget test","Trovare un leak a mano è utile una volta; evitarne la ricomparsa richiede un test. `leak_tracker_flutter_testing` consente di attivare il tracciamento per **tutti** i test della suite tramite il file speciale `test\u002Fflutter_test_config.dart`, che il test runner esegue automaticamente prima del `main` dei test.\n\nIl tracker funziona così: intercetta creazione e `dispose()` degli oggetti strumentati dal framework (controller, notifier, ecc.), poi forza il GC alla fine del test e verifica due invarianti:\n\n- `allNotDisposed`: nessun oggetto raccolto senza `dispose()`;\n- `allNotGCed`: nessun oggetto dismesso ma ancora raggiungibile.\n\nScrivi quindi un test che monta la schermata, la smonta e lascia che sia il tracker a emettere il verdetto. Nel test puoi anche verificare manualmente la \"radice\" con una `WeakReference`, tecnica preziosa per oggetti applicativi non strumentati dal framework (repository, controller custom, blocchi di logica).","\u002F\u002F test\u002Fflutter_test_config.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  LeakTracking.warnForUnsupportedPlatforms = false;\n\n  LeakTesting.settings = LeakTesting.settings\n      \u002F\u002F Ignora gli oggetti creati dagli helper di test (es. pumpWidget).\n      .withIgnoredAll(createdByTestHelpers: true)\n      .withTrackedAll();\n\n  await testMain();\n}","Lanciando `flutter test`, i test che lasciano oggetti non dismessi falliscono con un report che elenca classe, numero di istanze e stack di creazione.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":49,"expected_result":70,"demo_url":42,"video_url":42},753,5,"Il test di non-regressione: WeakReference e forzatura del GC","Il tracker copre gli oggetti del framework, ma spesso il leak riguarda classi tue. Il pattern definitivo è: crea l'oggetto, mantienine solo una `WeakReference`, smonta il widget, forza la garbage collection e verifica che il target sia diventato `null`.\n\nPer forzare il GC in un test si usa `leak_tracker`: `await forceGC()` esegue più cicli di raccolta attendendo tra uno e l'altro, evitando i falsi negativi tipici del GC generazionale. Evita assolutamente di trattenere il riferimento forte in una variabile locale del test: Dart potrebbe mantenerlo vivo fino a fine scope, rendendo il test instabile. Usa una funzione dedicata che restituisca solo la `WeakReference`.\n\nQuesto test, aggiunto alla suite, fallirà finché la schermata resta \"leaky\" e passerà dopo il fix del passo successivo: è la rete di sicurezza che vuoi in CI.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:leak_tracker\u002Fleak_tracker.dart';\n\nimport 'package:my_app\u002Fleaky_detail_page.dart';\n\nFuture\u003CWeakReference\u003CObject>> _mountAndCapture(WidgetTester tester) async {\n  await tester.pumpWidget(const MaterialApp(home: LeakyDetailPage(id: 1)));\n  await tester.pump(const Duration(milliseconds: 16));\n\n  final state = tester.state(find.byType(LeakyDetailPage));\n  final ref = WeakReference\u003CObject>(state);\n\n  \u002F\u002F Smonta l'albero: dopo questo punto nulla dovrebbe trattenere lo State.\n  await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));\n  await tester.pumpAndSettle();\n  return ref;\n}\n\nvoid main() {\n  testWidgets('la pagina di dettaglio non viene trattenuta dopo l\\'unmount',\n      (tester) async {\n    final ref = await _mountAndCapture(tester);\n\n    await forceGC(fullGcCycles: 3);\n\n    expect(\n      ref.target,\n      isNull,\n      reason: 'Lo State è ancora raggiungibile: controlla listener, '\n          'subscription e cache statiche.',\n    );\n  });\n}","Il test fallisce con il messaggio personalizzato finché il leak è presente: hai ora una prova riproducibile e automatica.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":49,"expected_result":77,"demo_url":42,"video_url":42},754,6,"Correggere: pattern di dispose a prova di dimenticanza","La correzione puntuale è ovvia (cancellare subscription, rimuovere listener, svuotare la cache). Il vero obiettivo però è rendere **strutturalmente difficile** dimenticarsene. Introduciamo un mixin `AutoDispose` che centralizza il cleanup: ogni risorsa viene registrata al momento della creazione e liberata in ordine inverso nel `dispose()`.\n\nRegole di progettazione che eliminano intere classi di leak:\n\n- **Chi crea, dismette**: se un widget riceve un controller dall'esterno, non deve dismetterlo (e viceversa). Documentalo nell'API.\n- **Niente cache statiche di oggetti con ciclo di vita UI**: se serve una cache, usa chiavi\u002Fvalori *dati*, mai `State`, `BuildContext` o widget.\n- **Mai catturare `BuildContext` in callback a lunga vita**: cattura solo i valori che ti servono, e proteggi con `if (!mounted) return;` dopo ogni `await`.\n- **Listener globali con scope**: preferisci stream con `StreamSubscription` gestita al `addListener` su singleton; se usi `ChangeNotifier`, accoppia sempre `addListener`\u002F`removeListener` nello stesso file e nella stessa classe.\n- **Chiudi i `StreamController`** che possiedi, anche quelli broadcast.","import 'dart:async';\nimport 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Centralizza il cleanup: tutto ciò che registri viene liberato in dispose().\nmixin AutoDispose\u003CT extends StatefulWidget> on State\u003CT> {\n  final List\u003CVoidCallback> _cleanups = \u003CVoidCallback>[];\n\n  \u002F\u002F\u002F Registra una funzione di cleanup arbitraria.\n  void onDispose(VoidCallback cleanup) => _cleanups.add(cleanup);\n\n  \u002F\u002F\u002F Ascolta uno stream cancellando automaticamente la subscription.\n  StreamSubscription\u003CE> listenTo\u003CE>(\n    Stream\u003CE> stream,\n    void Function(E event) onData, {\n    Function? onError,\n  }) {\n    final sub = stream.listen(onData, onError: onError);\n    onDispose(sub.cancel);\n    return sub;\n  }\n\n  \u002F\u002F\u002F Registra un listener su un Listenable rimuovendolo automaticamente.\n  void bindListener(Listenable listenable, VoidCallback listener) {\n    listenable.addListener(listener);\n    onDispose(() => listenable.removeListener(listener));\n  }\n\n  \u002F\u002F\u002F Registra un oggetto con dispose() (controller, notifier, focus node...).\n  C own\u003CC extends ChangeNotifier>(C notifier) {\n    onDispose(notifier.dispose);\n    return notifier;\n  }\n\n  @override\n  void dispose() {\n    for (final cleanup in _cleanups.reversed) {\n      cleanup();\n    }\n    _cleanups.clear();\n    super.dispose();\n  }\n}\n\nclass SafeDetailPage extends StatefulWidget {\n  const SafeDetailPage({super.key, required this.id});\n  final int id;\n\n  @override\n  State\u003CSafeDetailPage> createState() => _SafeDetailPageState();\n}\n\nclass _SafeDetailPageState extends State\u003CSafeDetailPage>\n    with SingleTickerProviderStateMixin, AutoDispose {\n  late final AnimationController _controller;\n  String _last = '-';\n\n  @override\n  void initState() {\n    super.initState();\n\n    _controller = own(AnimationController(\n      vsync: this,\n      duration: const Duration(seconds: 1),\n    ))\n      ..repeat();\n\n    listenTo\u003CString>(AppBus.instance.events, (e) {\n      if (!mounted) return;\n      setState(() => _last = e);\n    });\n\n    bindListener(AppBus.instance, _onBusChanged);\n  }\n\n  void _onBusChanged() {\u002F* ... *\u002F}\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Dettaglio ${widget.id}')),\n      body: Center(\n        child: RotationTransition(\n          turns: _controller,\n          child: Text('Ultimo evento: $_last'),\n        ),\n      ),\n    );\n  }\n}","Il test con WeakReference passa: dopo l'unmount lo State viene raccolto dal GC e il tracker non segnala oggetti non dismessi.",{"id":79,"position":80,"title":81,"body":82,"code_snippet":83,"code_language":49,"expected_result":84,"demo_url":42,"video_url":42},755,7,"Memoria nativa, cache immagini e controllo continuo in CI","Risolto il Dart heap, resta la memoria **nativa**, spesso la voce più pesante su mobile. Le immagini decodificate vivono in `ImageCache` e nel heap nativo di Skia\u002FImpeller: una lista con immagini a piena risoluzione può saturare la RAM anche senza alcun leak logico.\n\nAzioni concrete:\n\n- Ridimensiona in decodifica con `cacheWidth`\u002F`cacheHeight` oppure `ResizeImage`: decodificare un JPEG 4000×3000 per mostrarlo a 300 px costa ~48 MB contro ~0,4 MB.\n- Taratura esplicita di `imageCache.maximumSizeBytes` in base al target di dispositivo; svuota la cache sugli eventi di memoria critica (`didHaveMemoryPressure`).\n- Evita di trattenere `ui.Image` manuali senza chiamare `dispose()`: sono memoria nativa pura, invisibile nel Dart heap.\n\nInfine, rendi il controllo permanente: esegui la suite di test (che ora include il leak tracking) in CI. Un leak reintrodotto fa fallire la pipeline prima della code review, non tre release dopo in produzione. Per le campagne periodiche, pianifica una sessione manuale di profiling in profile mode sulle schermate più trafficate, seguendo il protocollo dei tre snapshot del passo 3.","import 'package:flutter\u002Fmaterial.dart';\n\nclass MemoryAwareApp extends StatefulWidget {\n  const MemoryAwareApp({super.key, required this.child});\n  final Widget child;\n\n  @override\n  State\u003CMemoryAwareApp> createState() => _MemoryAwareAppState();\n}\n\nclass _MemoryAwareAppState extends State\u003CMemoryAwareApp>\n    with WidgetsBindingObserver {\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n\n    final cache = PaintingBinding.instance.imageCache;\n    cache.maximumSizeBytes = 64 \u003C\u003C 20; \u002F\u002F 64 MB invece dei 100 di default\n    cache.maximumSize = 200;           \u002F\u002F numero massimo di immagini\n  }\n\n  @override\n  void didHaveMemoryPressure() {\n    PaintingBinding.instance.imageCache.clearLiveImages();\n    PaintingBinding.instance.imageCache.clear();\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) => widget.child;\n}\n\n\u002F\u002F Decodifica ridimensionata: enorme risparmio di memoria nativa.\nWidget thumbnail(String url) => Image.network(\n      url,\n      width: 150,\n      height: 150,\n      fit: BoxFit.cover,\n      cacheWidth: 300, \u002F\u002F 2x per schermi ad alta densità\n    );","Il picco di memoria sulle schermate con immagini si riduce sensibilmente e la CI fallisce automaticamente se un nuovo commit reintroduce oggetti non dismessi.",[],1792316928088]