[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-js-interop-in-flutter-web-chiamare-javascript-con-dartjs-interop-e-packageweb":3,"comments-article-js-interop-in-flutter-web-chiamare-javascript-con-dartjs-interop-e-packageweb":26},{"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},134,"JS interop in Flutter Web: chiamare JavaScript con dart:js_interop e package:web","js-interop-in-flutter-web-chiamare-javascript-con-dartjs-interop-e-packageweb","dart:html è deprecato e dart2wasm non lo supporta: oggi l'interoperabilità con JavaScript passa da dart:js_interop, extension type e package:web. Guida pratica con esempi reali, da Promise e callback fino al caricamento di SDK esterni.","Prima o poi capita a tutti: su Flutter Web serve una libreria JavaScript che non ha un equivalente Dart, oppure un'API del browser non ancora esposta. Fino a qualche tempo fa si usavano `dart:html` e `package:js`; oggi entrambi sono in disuso e la strada ufficiale è un'altra: **`dart:js_interop`** (static interop) insieme a **`package:web`**.\n\nIl motivo non è solo estetico. Il compilatore **dart2wasm** non supporta `dart:html` né l'interop dinamico di `package:js`: se vuoi compilare in WebAssembly, l'unica opzione è l'interop statico. Vediamo come funziona, con esempi che puoi incollare in un progetto reale.\n\n## Perché dart:html non basta più\n\n- `dart:html`, `dart:js` e `dart:js_util` sono **deprecati** (a partire da Dart 3.x) e non funzionano con dart2wasm.\n- `package:js` con `@JS`\u002F`@anonymous` dinamico è legacy: il sostituto è `dart:js_interop`, parte dell'SDK.\n- `package:web` è il wrapper moderno del DOM, generato dalle specifiche WebIDL: stessi nomi delle API del browser, nessuna \"traduzione\" dartificata.\n\nRequisiti: **Dart 3.3+ \u002F Flutter 3.19+**. Nel `pubspec.yaml`:\n\n```yaml\ndependencies:\n  web: ^1.1.0\n```\n\n## I tipi JS: come ragiona dart:js_interop\n\nLa libreria introduce una gerarchia di tipi che rappresentano valori JavaScript: `JSAny`, `JSObject`, `JSFunction`, `JSArray`, `JSPromise`, `JSString`, `JSNumber`, `JSBoolean`, `JSExportedDartFunction`.\n\nQuesti **non sono** valori Dart: sono \"handle\" verso l'altro mondo. La conversione è esplicita:\n\n```dart\nimport 'dart:js_interop';\n\nfinal JSString js = 'ciao'.toJS;   \u002F\u002F Dart -> JS\nfinal String dart = js.toDart;      \u002F\u002F JS -> Dart\n\nfinal JSArray\u003CJSString> lista = \u003CJSString>['a'.toJS, 'b'.toJS].toJS;\nfinal List\u003CString> back = lista.toDart.map((e) => e.toDart).toList();\n```\n\nRegola pratica: nelle firme `external` puoi usare direttamente i tipi Dart primitivi (`String`, `int`, `double`, `num`, `bool`, `void`, `Null`), che vengono convertiti automaticamente. Per tutto il resto servono i tipi `JS*`.\n\n## Primo contatto: chiamare funzioni globali\n\nUna funzione globale JavaScript si dichiara con `@JS` e `external`, senza corpo:\n\n```dart\nimport 'dart:js_interop';\n\n@JS('alert')\nexternal void showAlert(String message);\n\n@JS('console.log')\nexternal void consoleLog(JSAny? value);\n\n@JS('encodeURIComponent')\nexternal String encodeUriComponentJs(String value);\n```\n\nSe la funzione vive dentro un namespace, puoi dichiararlo una volta sola a livello di libreria:\n\n```dart\n@JS('myLib')\nlibrary my_lib_interop;\n\nimport 'dart:js_interop';\n\n@JS('version')\nexternal String get version; \u002F\u002F -> myLib.version\n\n@JS('init')\nexternal void init(); \u002F\u002F -> myLib.init()\n```\n\nPer verificare a runtime se un simbolo esiste (utile con SDK caricati via `\u003Cscript>`):\n\n```dart\nimport 'dart:js_interop';\nimport 'dart:js_interop_unsafe';\n\nbool get isLibLoaded => globalContext.has('myLib');\n```\n\n## Descrivere oggetti JS con gli extension type\n\nIl cuore dell'interop moderno sono gli **extension type**: wrapper a costo zero su `JSObject` che descrivono la \"forma\" di un oggetto JavaScript.\n\nImmaginiamo una libreria `MyPlayer` con costruttore, metodi, getter e un campo callback:\n\n```dart\nimport 'dart:js_interop';\n\n@JS('MyPlayer')\nextension type MyPlayer._(JSObject _) implements JSObject {\n  external MyPlayer(String containerId, JSObject options);\n\n  external void play();\n  external void pause();\n  external void destroy();\n\n  external String get currentSrc;\n  external double get duration;\n\n  external JSPromise\u003CJSBoolean> load(String url);\n\n  external set onEnded(JSFunction callback);\n}\n```\n\nPer gli oggetti di configurazione \"anonimi\" (i classici object literal) si usa un extension type con costruttore `external factory` e parametri nominati:\n\n```dart\nextension type PlayerOptions._(JSObject _) implements JSObject {\n  external factory PlayerOptions({\n    bool autoplay,\n    bool muted,\n    double volume,\n  });\n\n  external bool get autoplay;\n}\n```\n\nUso dal lato Dart:\n\n```dart\nfinal player = MyPlayer(\n  'player-container',\n  PlayerOptions(autoplay: true, muted: true, volume: 0.8),\n);\nplayer.play();\n```\n\nNota importante: un extension type **non** aggiunge controlli a runtime. Se l'oggetto JS non ha davvero quel metodo, l'errore arriva solo al momento della chiamata. Gli extension type sono documentazione tipizzata, non una validazione.\n\n## Promise e Future\n\nLe `Promise` JavaScript si convertono in `Future` con `.toDart`:\n\n```dart\nFuture\u003Cbool> loadTrack(MyPlayer player, String url) async {\n  try {\n    final JSBoolean result = await player.load(url).toDart;\n    return result.toDart;\n  } catch (e) {\n    \u002F\u002F L'errore arriva dal mondo JS: può essere un JSObject (Error), una\n    \u002F\u002F stringa, qualsiasi cosa. Non assumere un tipo specifico.\n    debugPrint('Errore di caricamento: $e');\n    return false;\n  }\n}\n```\n\nSe la Promise risolve `undefined`, dichiara `JSPromise\u003CJSAny?>` e ignora il valore. Al contrario, per esporre un `Future` Dart a JavaScript esiste `.toJS` su `Future`, che produce una `JSPromise`.\n\n## Callback: da Dart a JavaScript\n\nUna funzione Dart diventa richiamabile da JS con `.toJS`:\n\n```dart\nvoid attachHandlers(MyPlayer player) {\n  player.onEnded = (() {\n    debugPrint('Riproduzione terminata');\n  }).toJS;\n}\n```\n\nSe la callback riceve argomenti, devono essere tipi interop:\n\n```dart\nextension type ProgressEvent._(JSObject _) implements JSObject {\n  external double get percent;\n}\n\nvoid onProgress(ProgressEvent event) {\n  debugPrint('Avanzamento: ${event.percent}%');\n}\n\nfinal jsCallback = onProgress.toJS;\n```\n\nDue accortezze:\n\n- `.toJS` su una funzione crea **ogni volta un nuovo oggetto JS**: se devi rimuovere un listener, conserva il riferimento.\n- Le callback che devono sopravvivere al widget vanno \"spente\" nel `dispose()`, altrimenti rischi riferimenti pendenti e memory leak.\n\n## package:web: il DOM senza sorprese\n\n`package:web` espone le API del browser con i nomi originali:\n\n```dart\nimport 'package:web\u002Fweb.dart' as web;\n\nvoid removeSplash() {\n  web.document.querySelector('#loading')?.remove();\n}\n\nvoid saveToken(String token) {\n  web.window.localStorage.setItem('token', token);\n}\n\nString? readToken() => web.window.localStorage.getItem('token');\n\nbool get isOnline => web.window.navigator.onLine;\n```\n\nGli eventi si gestiscono con `addEventListener` e una `JSFunction`:\n\n```dart\nimport 'dart:js_interop';\nimport 'package:web\u002Fweb.dart' as web;\n\nvoid listenVisibility(void Function(bool visible) onChange) {\n  web.document.addEventListener(\n    'visibilitychange',\n    (web.Event _) {\n      onChange(web.document.visibilityState == 'visible');\n    }.toJS,\n  );\n}\n```\n\n## Caricare un SDK esterno a runtime\n\nSpesso non vuoi inserire lo `\u003Cscript>` in `web\u002Findex.html` (per esempio per caricarlo solo dopo il consenso ai cookie). Puoi iniettarlo dinamicamente:\n\n```dart\nimport 'dart:async';\nimport 'dart:js_interop';\nimport 'package:web\u002Fweb.dart' as web;\n\nFuture\u003Cvoid> loadScript(String src) {\n  final completer = Completer\u003Cvoid>();\n\n  final script = web.HTMLScriptElement()\n    ..src = src\n    ..async = true;\n\n  script.addEventListener('load', (web.Event _) {\n    if (!completer.isCompleted) completer.complete();\n  }.toJS);\n\n  script.addEventListener('error', (web.Event _) {\n    if (!completer.isCompleted) {\n      completer.completeError(StateError('Impossibile caricare $src'));\n    }\n  }.toJS);\n\n  web.document.head!.appendChild(script);\n  return completer.future;\n}\n```\n\nE poi, prima di usare la libreria:\n\n```dart\nFuture\u003Cvoid> ensureSdk() async {\n  if (globalContext.has('myLib')) return;\n  await loadScript('https:\u002F\u002Fcdn.example.com\u002Fmylib.min.js');\n}\n```\n\n## Passare dati strutturati: jsify e dartify\n\nPer mappe e liste \"generiche\" esistono due scorciatoie:\n\n```dart\nimport 'dart:js_interop';\n\nfinal payload = \u003CString, Object?>{\n  'userId': 42,\n  'tags': ['flutter', 'web'],\n  'premium': true,\n}.jsify() as JSObject;\n\nfinal Object? back = payload.dartify(); \u002F\u002F Map\u003CObject?, Object?>\n```\n\n`jsify()`\u002F`dartify()` fanno una conversione **profonda**: comode per JSON, costose su strutture grandi e in un loop. Quando la forma dei dati è nota, preferisci un extension type tipizzato: è più veloce e autodocumentante.\n\n## Isolare il codice web dal resto dell'app\n\n`dart:js_interop` e `package:web` non compilano su Android, iOS o desktop. La soluzione standard sono le **conditional import**:\n\n```dart\n\u002F\u002F lib\u002Fsrc\u002Fweb_share\u002Fweb_share.dart\nexport 'web_share_stub.dart'\n    if (dart.library.js_interop) 'web_share_web.dart';\n```\n\n```dart\n\u002F\u002F lib\u002Fsrc\u002Fweb_share\u002Fweb_share_stub.dart\nclass WebShare {\n  static bool get isSupported => false;\n  static Future\u003Cbool> share({required String text, String? url}) async => false;\n}\n```\n\n```dart\n\u002F\u002F lib\u002Fsrc\u002Fweb_share\u002Fweb_share_web.dart\nimport 'dart:js_interop';\n\n@JS('navigator')\nexternal _Navigator get _navigator;\n\nextension type _Navigator._(JSObject _) implements JSObject {\n  external JSPromise\u003CJSAny?> share(JSObject data);\n  external bool canShare(JSObject data);\n}\n\nextension type _ShareData._(JSObject _) implements JSObject {\n  external factory _ShareData({String? title, String? text, String? url});\n}\n\nclass WebShare {\n  static bool get isSupported => _navigator.has('share');\n\n  static Future\u003Cbool> share({required String text, String? url}) async {\n    if (!isSupported) return false;\n    try {\n      await _navigator.share(_ShareData(text: text, url: url)).toDart;\n      return true;\n    } catch (_) {\n      \u002F\u002F L'utente ha annullato (AbortError) o il browser ha rifiutato.\n      return false;\n    }\n  }\n}\n```\n\n`has` arriva da `dart:js_interop_unsafe`, da importare nel file web. Nel resto dell'app importi solo `web_share.dart` e scrivi codice che compila ovunque:\n\n```dart\nif (WebShare.isSupported) {\n  await WebShare.share(text: 'Guarda questo articolo', url: articleUrl);\n} else {\n  await Share.share(articleUrl); \u002F\u002F fallback con share_plus\n}\n```\n\n## Chiamare Dart da JavaScript\n\nPer esporre una funzione Dart al mondo JS (tipico per i callback di SDK di pagamento o analytics inseriti in `index.html`):\n\n```dart\nimport 'dart:js_interop';\nimport 'dart:js_interop_unsafe';\n\nvoid registerBridge() {\n  globalContext['onFlutterReady'] = (() {\n    debugPrint('Flutter è pronto, chiamato da JS');\n  }).toJS;\n}\n```\n\nSe devi passare a JavaScript un oggetto Dart opaco (da restituirti intatto più avanti), usa `JSBoxedDartObject` tramite `toJSBox` \u002F `toDartObject`, invece di tentare conversioni manuali.\n\n## Errori comuni (e come evitarli)\n\n- **Usare ancora `dart:html`**: blocca la compilazione con dart2wasm. Migra a `package:web` (le API hanno nomi quasi identici a quelle del browser).\n- **Dimenticare `.toDart` \u002F `.toJS`**: `JSString` e `String` non sono intercambiabili, nemmeno in `print`.\n- **Fidarsi degli extension type**: non verificano nulla a runtime. Controlla sempre con `has(...)` che l'SDK sia caricato.\n- **Catturare un tipo di eccezione specifico**: gli errori JS si presentano diversamente su dart2js e dart2wasm. Usa `catch (e)` generico.\n- **`jsify()` ovunque**: usalo solo per payload dinamici, non nei percorsi caldi.\n- **Non rimuovere i listener**: ogni `.toJS` crea una nuova funzione; conserva il riferimento per `removeEventListener`.\n- **Testare solo con dart2js**: se punti al Wasm, verifica con `flutter build web --wasm` fin da subito.\n\n## Checklist finale\n\n1. `package:web` al posto di `dart:html`, `dart:js_interop` al posto di `package:js`.\n2. Tipizza le API esterne con extension type, un file per SDK.\n3. Isola tutto dietro conditional import con uno stub per le altre piattaforme.\n4. Converti esplicitamente ai confini (`toJS`\u002F`toDart`), mai in mezzo alla logica di business.\n5. Avvolgi l'interop in un servizio Dart ordinario, così i widget non vedono mai un `JSObject`.\n\nCon questo schema l'interoperabilità smette di essere un punto fragile: diventa un semplice adapter, testabile e pronto anche per la compilazione in WebAssembly.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F147ac566-39c3-485a-898a-41479499cbd9.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1610986602726-19f650133f7a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTI1NTUzNDF8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Ferenc Almasi","https:\u002F\u002Funsplash.com\u002F@flowforfrank","https:\u002F\u002Funsplash.com\u002Fphotos\u002Ftext-cWUPyE8qggw",null,"published","2026-10-21T04:02:21+00:00","JS interop in Flutter Web con dart:js_interop","Guida pratica all'interop JavaScript in Flutter Web: dart:js_interop, package:web, extension type, Promise, callback e conditional import, pronti per Wasm.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792578489807]