[{"data":1,"prerenderedAt":71},["ShallowReactive",2],{"tutorial-isolate-e-compute-in-flutter-lavoro-pesante-fuori-dal-thread-ui":3,"comments-tutorial-isolate-e-compute-in-flutter-lavoro-pesante-fuori-dal-thread-ui":70},{"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":33},86,"Isolate e compute in Flutter: lavoro pesante fuori dal thread UI","isolate-e-compute-in-flutter-lavoro-pesante-fuori-dal-thread-ui","Impara a spostare elaborazioni CPU-intensive fuori dal thread UI con compute, Isolate.run, isolate a lunga vita con SendPort\u002FReceivePort e TransferableTypedData.","Flutter esegue build, layout e paint in un singolo thread: l'**UI isolate**. Qualsiasi operazione sincrona che superi i ~16 ms fa saltare frame e produce jank visibile.\n\nDart non usa i thread condivisi: usa gli **isolate**, unità di esecuzione con heap separato che comunicano solo tramite messaggi. Questo elimina lock e race condition, ma impone regole precise su cosa si può inviare.\n\nIn questo tutorial avanzato vedremo:\n\n- quando conviene davvero usare un isolate (e quando no);\n- `compute()` e `Isolate.run()` per i task one-shot;\n- un isolate **a lunga vita** con `ReceivePort`\u002F`SendPort` per flussi di richieste;\n- cancellazione, gestione errori e `TransferableTypedData` per lo zero-copy;\n- come misurare il guadagno con i DevTools.\n\n> Prerequisiti: Dart 3, dimestichezza con `async`\u002F`await` e `Stream`.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Ffed70e42-8d71-4049-b690-06ac0317c4e8.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1756921117941-afb0976ab976?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODg2NjkyMTZ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Edgar Cornejo","https:\u002F\u002Funsplash.com\u002F@devcornejo","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fclose-up-of-a-computer-circuit-board-with-many-components-2a_ViSfg3tw","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=-BjY_SVrS1A",{"youtube_id":17,"duration_seconds":18,"published_at":19},"-BjY_SVrS1A",124,"2026-09-06T09:02:01+00:00","advanced",45,"3.x","published","2026-09-06T04:33:36+00:00","Isolate e compute in Flutter: guida avanzata","Sposta il lavoro pesante fuori dal thread UI in Flutter: compute, Isolate.run, isolate a lunga vita con SendPort e TransferableTypedData.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,49,56,63],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},573,"Riprodurre il jank: il problema del thread UI","Prima di ottimizzare bisogna misurare. Creiamo un task volutamente costoso (parsing di un JSON grande + aggregazione) e chiamiamolo direttamente dal `build` context di un bottone.\n\nCon un'animazione attiva (es. `CircularProgressIndicator`) vedrai l'indicatore **congelarsi** per centinaia di millisecondi.\n\nAttiva anche l'overlay delle performance (`flutter run --profile`, poi `P` nel terminale o Performance Overlay nei DevTools): noterai le barre rosse sul grafico dell'UI thread.\n\n> Nota: gli isolate hanno un costo di avvio (~1-2 ms) e di serializzazione dei messaggi. Sotto i ~10 ms di lavoro, restare sull'UI isolate è più veloce.","import 'dart:convert';\n\n\u002F\u002F\u002F Simula un payload pesante (~20k record).\nString buildFakePayload() {\n  final items = List.generate(20000, (i) => {\n        'id': i,\n        'name': 'Prodotto \\$i',\n        'price': (i % 97) * 1.37,\n        'tags': ['a', 'b', 'c'],\n      });\n  return jsonEncode({'items': items});\n}\n\n\u002F\u002F\u002F Task CPU-bound: decode + aggregazione.\ndouble heavyParse(String raw) {\n  final map = jsonDecode(raw) as Map\u003CString, dynamic>;\n  final items = (map['items'] as List).cast\u003CMap\u003CString, dynamic>>();\n  var total = 0.0;\n  for (final item in items) {\n    total += (item['price'] as num).toDouble();\n  }\n  return total;\n}\n\n\u002F\u002F Chiamata bloccante: NON fare così in produzione.\nvoid onPressedBlocking() {\n  final total = heavyParse(buildFakePayload());\n  debugPrint('Totale: \\$total');\n}","dart","L'animazione si blocca visibilmente durante il calcolo e il Performance Overlay mostra frame oltre i 16 ms.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},574,2,"Il primo rimedio: compute() e Isolate.run()","Per un task **one-shot** hai due strumenti:\n\n- `compute(fn, message)` di `package:flutter\u002Ffoundation.dart`: crea un isolate, esegue `fn` e lo distrugge. Su Flutter Web degrada elegantemente a esecuzione sincrona.\n- `Isolate.run(() => ...)` di `dart:isolate` (Dart 2.19+): API più moderna, accetta una closure e supporta il *capture* delle variabili, ma **non** funziona su Web.\n\nRegole fondamentali:\n\n1. La funzione passata a `compute` deve essere **top-level** o **static** (non può catturare `this`).\n2. Il messaggio deve essere serializzabile: primitive, `List`, `Map`, `TypedData`, `SendPort`, istanze di classi *deeply immutable*. **Niente** oggetti Flutter come `BuildContext`, `Image` o handle di plugin.\n3. Un isolate secondario **non può chiamare la maggior parte dei plugin** (i platform channel richiedono il root isolate) a meno di usare `BackgroundIsolateBinaryMessenger.ensureInitialized(rootToken)`.","import 'dart:isolate';\nimport 'package:flutter\u002Ffoundation.dart';\n\n\u002F\u002F 1) compute: funzione top-level obbligatoria\nFuture\u003Cdouble> parseWithCompute(String raw) {\n  return compute(heavyParse, raw, debugLabel: 'heavyParse');\n}\n\n\u002F\u002F 2) Isolate.run: closure, più flessibile (no Web)\nFuture\u003Cdouble> parseWithRun(String raw) {\n  return Isolate.run(() => heavyParse(raw));\n}\n\n\u002F\u002F Uso nella UI\nFuture\u003Cvoid> _onPressed() async {\n  setState(() => _loading = true);\n  try {\n    final total = await parseWithCompute(buildFakePayload());\n    if (!mounted) return;\n    setState(() => _total = total);\n  } finally {\n    if (mounted) setState(() => _loading = false);\n  }\n}","L'indicatore di caricamento gira fluido durante il parsing: nessun frame perso sull'UI thread.",{"id":50,"position":51,"title":52,"body":53,"code_snippet":54,"code_language":39,"expected_result":55,"demo_url":41,"video_url":41},575,3,"Isolate a lunga vita: worker con ReceivePort e SendPort","`compute` paga l'avvio dell'isolate ad ogni chiamata. Se devi processare **molte richieste** (es. decodifica continua di messaggi WebSocket, filtri su immagini, ricerca full-text) conviene un **worker persistente**.\n\nIl protocollo classico è un handshake in due tempi:\n\n1. L'isolate principale crea un `ReceivePort` e passa il suo `sendPort` a `Isolate.spawn`.\n2. Il worker crea a sua volta un `ReceivePort` e rispedisce indietro il proprio `SendPort`.\n3. Da quel momento si scambiano messaggi identificati da un `id` per correlare richiesta e risposta.\n\nQui sotto un worker riutilizzabile con `Completer` per mappare le risposte.","import 'dart:async';\nimport 'dart:isolate';\n\nclass _Request {\n  const _Request(this.id, this.payload);\n  final int id;\n  final String payload;\n}\n\nclass _Response {\n  const _Response(this.id, this.result, this.error);\n  final int id;\n  final double? result;\n  final Object? error;\n}\n\nclass ParserWorker {\n  ParserWorker._(this._isolate, this._toWorker, this._fromWorker) {\n    _fromWorker.listen(_handle);\n  }\n\n  final Isolate _isolate;\n  final SendPort _toWorker;\n  final ReceivePort _fromWorker;\n  final _pending = \u003Cint, Completer\u003Cdouble>>{};\n  int _nextId = 0;\n\n  static Future\u003CParserWorker> spawn() async {\n    final init = ReceivePort();\n    final isolate = await Isolate.spawn(_entryPoint, init.sendPort);\n    final toWorker = await init.first as SendPort;\n    init.close();\n    final fromWorker = ReceivePort();\n    toWorker.send(fromWorker.sendPort);\n    return ParserWorker._(isolate, toWorker, fromWorker);\n  }\n\n  Future\u003Cdouble> parse(String payload) {\n    final id = _nextId++;\n    final completer = Completer\u003Cdouble>();\n    _pending[id] = completer;\n    _toWorker.send(_Request(id, payload));\n    return completer.future;\n  }\n\n  void _handle(dynamic message) {\n    final res = message as _Response;\n    final completer = _pending.remove(res.id);\n    if (completer == null) return;\n    if (res.error != null) {\n      completer.completeError(res.error!);\n    } else {\n      completer.complete(res.result!);\n    }\n  }\n\n  void dispose() {\n    for (final c in _pending.values) {\n      c.completeError(StateError('Worker terminato'));\n    }\n    _pending.clear();\n    _fromWorker.close();\n    _isolate.kill(priority: Isolate.immediate);\n  }\n\n  static void _entryPoint(SendPort initPort) {\n    final commands = ReceivePort();\n    initPort.send(commands.sendPort);\n    SendPort? replyTo;\n    commands.listen((message) {\n      if (message is SendPort) {\n        replyTo = message;\n        return;\n      }\n      final req = message as _Request;\n      try {\n        replyTo?.send(_Response(req.id, heavyParse(req.payload), null));\n      } catch (e) {\n        replyTo?.send(_Response(req.id, null, e.toString()));\n      }\n    });\n  }\n}","Un worker unico gestisce N richieste concorrenti: il costo di spawn si paga una sola volta e ogni `parse()` risolve la propria Future.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":39,"expected_result":62,"demo_url":41,"video_url":41},576,4,"Cancellazione, errori e ciclo di vita nel widget","Un isolate non si interrompe da solo. Servono tre accortezze:\n\n- **Kill esplicito**: `isolate.kill(priority: Isolate.immediate)` in `dispose()`, altrimenti resta vivo e consuma memoria.\n- **Errori non catturati**: registra un `onError` port in `Isolate.spawn` per non perdere le eccezioni.\n- **Cooperative cancellation**: per loop lunghi, fai controllare periodicamente al worker un flag ricevuto via porta e interrompi il ciclo.\n\nNel widget, lega lo spawn a `initState` (con `Future` memorizzata) e la distruzione a `dispose`, ricordando il classico controllo `mounted` dopo ogni `await`.","class ParserScreen extends StatefulWidget {\n  const ParserScreen({super.key});\n  @override\n  State\u003CParserScreen> createState() => _ParserScreenState();\n}\n\nclass _ParserScreenState extends State\u003CParserScreen> {\n  ParserWorker? _worker;\n  double? _total;\n  bool _busy = false;\n\n  @override\n  void initState() {\n    super.initState();\n    ParserWorker.spawn().then((w) {\n      if (!mounted) {\n        w.dispose();\n        return;\n      }\n      setState(() => _worker = w);\n    });\n  }\n\n  Future\u003Cvoid> _run() async {\n    final worker = _worker;\n    if (worker == null || _busy) return;\n    setState(() => _busy = true);\n    try {\n      final total = await worker.parse(buildFakePayload());\n      if (!mounted) return;\n      setState(() => _total = total);\n    } catch (e) {\n      if (!mounted) return;\n      ScaffoldMessenger.of(context)\n          .showSnackBar(SnackBar(content: Text('Errore: \\$e')));\n    } finally {\n      if (mounted) setState(() => _busy = false);\n    }\n  }\n\n  @override\n  void dispose() {\n    _worker?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            const CircularProgressIndicator(),\n            const SizedBox(height: 24),\n            Text(_total?.toStringAsFixed(2) ?? '—'),\n            const SizedBox(height: 16),\n            FilledButton(\n              onPressed: _worker == null || _busy ? null : _run,\n              child: const Text('Elabora'),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}","Nessun isolate orfano alla chiusura della schermata; gli errori del worker arrivano alla UI come SnackBar.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":39,"expected_result":69,"demo_url":41,"video_url":41},577,5,"Zero-copy con TransferableTypedData e misurazione nei DevTools","I messaggi tra isolate vengono **copiati**. Con payload binari grandi (immagini, file, buffer audio) la copia può annullare il vantaggio del parallelismo.\n\n`TransferableTypedData` risolve il problema: il buffer viene **trasferito** (ownership move), senza copia. Attenzione: dopo `materialize()` il buffer non è più utilizzabile dal mittente.\n\nPer misurare il risultato:\n\n1. Lancia in `--profile` (mai in debug: la JIT falsa i tempi).\n2. Apri **DevTools → Performance**: nella timeline vedrai una track per ogni isolate; l'UI thread deve restare sotto i 16 ms.\n3. Usa `Timeline.timeSync('label', () { ... })` da `dart:developer` per marcare le sezioni del worker.\n4. In **Memory** verifica che gli isolate terminati spariscano dalla lista.\n\n**Checklist finale:** usa `compute`\u002F`Isolate.run` per task singoli > 10 ms; un worker persistente per flussi continui; `TransferableTypedData` per i binari; `kill()` sempre in `dispose()`.","import 'dart:developer' as dev;\nimport 'dart:isolate';\nimport 'dart:typed_data';\n\n\u002F\u002F\u002F Inverte i byte di un buffer senza copiarlo tra isolate.\nFuture\u003CUint8List> invertBytes(Uint8List input) async {\n  final transferable = TransferableTypedData.fromList([input]);\n  return Isolate.run(() {\n    return dev.Timeline.timeSync('invertBytes', () {\n      final bytes = transferable.materialize().asUint8List();\n      for (var i = 0; i \u003C bytes.length; i++) {\n        bytes[i] = 255 - bytes[i];\n      }\n      return bytes;\n    });\n  });\n}","Il trasferimento di buffer da decine di MB avviene senza picchi di memoria e la timeline dei DevTools mostra l'evento 'invertBytes' sull'isolate secondario, con l'UI thread libero.",[],1789205507461]