[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-flutter-desktop-creare-app-native-per-windows-macos-e-linux":3,"comments-article-flutter-desktop-creare-app-native-per-windows-macos-e-linux":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},77,"Flutter Desktop: creare app native per Windows, macOS e Linux","flutter-desktop-creare-app-native-per-windows-macos-e-linux","Flutter non è solo mobile: con il supporto desktop stabile si possono distribuire app native per Windows, macOS e Linux dallo stesso codice. Guida a setup, gestione della finestra, menu nativi, drag & drop di file, system tray e packaging.","## Perché Flutter Desktop oggi\n\nIl supporto desktop di Flutter è stabile da diverse release ormai e non è più una curiosità sperimentale: molte aziende distribuiscono già tool interni, editor e client desktop scritti interamente in Dart. Il vantaggio è evidente — lo stesso codebase di un'app mobile può girare su Windows, macOS e Linux — ma il rischio è altrettanto evidente: pubblicare su desktop un'app pensata per il touch produce un risultato che sembra \"un'app Android ingrandita\".\n\nIn questo articolo vediamo cosa serve davvero per portare un progetto Flutter su desktop in modo credibile: configurazione della finestra, menu di sistema nativi, scorciatoie da tastiera, drag & drop di file, system tray e distribuzione.\n\n## Abilitare e verificare il supporto desktop\n\nIl supporto è incluso nel canale stable. Verifichiamo che sia attivo:\n\n```bash\nflutter config --enable-windows-desktop\nflutter config --enable-macos-desktop\nflutter config --enable-linux-desktop\n\nflutter devices\n```\n\nSe il progetto esiste già ma è stato creato senza le piattaforme desktop, basta rigenerare le cartelle native:\n\n```bash\nflutter create --platforms=windows,macos,linux .\n```\n\nQuesto comando aggiunge `windows\u002F`, `macos\u002F` e `linux\u002F` senza toccare `lib\u002F`. A questo punto:\n\n```bash\nflutter run -d windows   # oppure macos \u002F linux\n```\n\n### Requisiti di build\n\n| Piattaforma | Toolchain necessaria |\n|---|---|\n| Windows | Visual Studio con workload \"Desktop development with C++\" |\n| macOS | Xcode + CocoaPods |\n| Linux | `clang`, `cmake`, `ninja-build`, `libgtk-3-dev`, `pkg-config` |\n\nSu Linux, su una Debian\u002FUbuntu pulita:\n\n```bash\nsudo apt install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev\n```\n\n## Distinguere il desktop dal mobile nel codice\n\nEvitate di sparpagliare `if (Platform.isWindows)` in tutta l'app. Meglio centralizzare in un piccolo helper, ricordando che `dart:io` **non** è disponibile su Web:\n\n```dart\nimport 'dart:io' show Platform;\nimport 'package:flutter\u002Ffoundation.dart';\n\nabstract final class AppPlatform {\n  static bool get isDesktop =>\n      !kIsWeb &&\n      (Platform.isWindows || Platform.isMacOS || Platform.isLinux);\n\n  static bool get isMobile =>\n      !kIsWeb && (Platform.isAndroid || Platform.isIOS);\n\n  \u002F\u002F\u002F Su desktop la scorciatoia primaria è Ctrl, su macOS è Cmd.\n  static bool get usesCommandKey => !kIsWeb && Platform.isMacOS;\n}\n```\n\nPer scelte puramente di layout, però, la domanda giusta non è \"su che sistema operativo sono?\" ma \"quanto spazio ho?\". Un tablet Android in landscape e un laptop hanno esigenze simili: usate `LayoutBuilder` per la struttura e `AppPlatform` solo per le funzionalità realmente specifiche della piattaforma (menu, finestre, tray).\n\n## Controllare la finestra con window_manager\n\nSu desktop l'app *è* una finestra: dimensione minima, titolo, posizione e comportamento alla chiusura sono responsabilità nostra. Il pacchetto `window_manager` copre tutti questi aspetti.\n\n```yaml\ndependencies:\n  window_manager: ^0.4.3\n```\n\nInizializzazione nel `main`:\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:window_manager\u002Fwindow_manager.dart';\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  if (AppPlatform.isDesktop) {\n    await windowManager.ensureInitialized();\n\n    const options = WindowOptions(\n      size: Size(1280, 800),\n      minimumSize: Size(900, 600),\n      center: true,\n      title: 'Gestionale Pro',\n      titleBarStyle: TitleBarStyle.normal,\n    );\n\n    await windowManager.waitUntilReadyToShow(options, () async {\n      await windowManager.show();\n      await windowManager.focus();\n    });\n  }\n\n  runApp(const MyApp());\n}\n```\n\n`waitUntilReadyToShow` evita il classico \"flash\" della finestra bianca prima che Flutter abbia disegnato il primo frame: la finestra viene mostrata solo quando è pronta. Ricordate di impostare `windowOptions.show: false` nel codice nativo se volete il controllo totale (su Windows si edita `windows\u002Frunner\u002Fmain.cpp`, ma nella maggior parte dei casi la configurazione Dart basta).\n\n### Intercettare la chiusura per chiedere conferma\n\nUn'app desktop che perde dati non salvati alla chiusura è inaccettabile. `window_manager` permette di intercettare l'evento:\n\n```dart\nclass EditorPage extends StatefulWidget {\n  const EditorPage({super.key});\n  @override\n  State\u003CEditorPage> createState() => _EditorPageState();\n}\n\nclass _EditorPageState extends State\u003CEditorPage> with WindowListener {\n  bool _hasUnsavedChanges = true;\n\n  @override\n  void initState() {\n    super.initState();\n    windowManager.addListener(this);\n    windowManager.setPreventClose(true);\n  }\n\n  @override\n  void dispose() {\n    windowManager.removeListener(this);\n    super.dispose();\n  }\n\n  @override\n  void onWindowClose() async {\n    if (!_hasUnsavedChanges) {\n      await windowManager.destroy();\n      return;\n    }\n    final confirm = await showDialog\u003Cbool>(\n      context: context,\n      builder: (_) => AlertDialog(\n        title: const Text('Modifiche non salvate'),\n        content: const Text('Vuoi uscire senza salvare?'),\n        actions: [\n          TextButton(\n            onPressed: () => Navigator.pop(context, false),\n            child: const Text('Annulla'),\n          ),\n          FilledButton(\n            onPressed: () => Navigator.pop(context, true),\n            child: const Text('Esci'),\n          ),\n        ],\n      ),\n    );\n    if (confirm ?? false) await windowManager.destroy();\n  }\n\n  @override\n  Widget build(BuildContext context) => const Placeholder();\n}\n```\n\n## Menu nativi con PlatformMenuBar\n\nSu macOS la barra dei menu è in cima allo schermo ed è un requisito di fatto per qualunque app seria. Flutter offre `PlatformMenuBar` che, su macOS, produce un menu **realmente nativo** (su Windows e Linux il widget viene ignorato, quindi lì serve una menu bar disegnata in Flutter o `MenuBar` di Material 3).\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter\u002Fservices.dart';\n\nWidget buildWithMenus(BuildContext context, Widget child) {\n  return PlatformMenuBar(\n    menus: [\n      PlatformMenu(\n        label: 'File',\n        menus: [\n          PlatformMenuItem(\n            label: 'Nuovo documento',\n            shortcut: const SingleActivator(LogicalKeyboardKey.keyN, meta: true),\n            onSelected: () => debugPrint('Nuovo'),\n          ),\n          PlatformMenuItem(\n            label: 'Salva',\n            shortcut: const SingleActivator(LogicalKeyboardKey.keyS, meta: true),\n            onSelected: () => debugPrint('Salva'),\n          ),\n          const PlatformMenuItemGroup(\n            members: [\n              PlatformProvidedMenuItem(\n                type: PlatformProvidedMenuItemType.quit,\n              ),\n            ],\n          ),\n        ],\n      ),\n    ],\n    child: child,\n  );\n}\n```\n\nPer Windows e Linux, la `MenuBar` di Material 3 offre un'esperienza coerente disegnata da Flutter:\n\n```dart\nMenuBar(\n  children: [\n    SubmenuButton(\n      menuChildren: [\n        MenuItemButton(\n          shortcut: const SingleActivator(LogicalKeyboardKey.keyN, control: true),\n          onPressed: () {},\n          child: const Text('Nuovo documento'),\n        ),\n        MenuItemButton(\n          onPressed: () {},\n          child: const Text('Esci'),\n        ),\n      ],\n      child: const Text('File'),\n    ),\n  ],\n)\n```\n\n## Drag & drop di file dal sistema operativo\n\nTrascinare un file dall'Esplora risorse dentro l'app è un gesto che gli utenti desktop danno per scontato. Il pacchetto `desktop_drop` lo rende banale:\n\n```yaml\ndependencies:\n  desktop_drop: ^0.5.0\n  cross_file: ^0.3.4\n```\n\n```dart\nimport 'package:cross_file\u002Fcross_file.dart';\nimport 'package:desktop_drop\u002Fdesktop_drop.dart';\nimport 'package:flutter\u002Fmaterial.dart';\n\nclass DropZone extends StatefulWidget {\n  const DropZone({super.key, required this.onFiles});\n  final ValueChanged\u003CList\u003CXFile>> onFiles;\n\n  @override\n  State\u003CDropZone> createState() => _DropZoneState();\n}\n\nclass _DropZoneState extends State\u003CDropZone> {\n  bool _hovering = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return DropTarget(\n      onDragEntered: (_) => setState(() => _hovering = true),\n      onDragExited: (_) => setState(() => _hovering = false),\n      onDragDone: (detail) {\n        setState(() => _hovering = false);\n        widget.onFiles(detail.files);\n      },\n      child: AnimatedContainer(\n        duration: const Duration(milliseconds: 150),\n        decoration: BoxDecoration(\n          border: Border.all(\n            color: _hovering\n                ? Theme.of(context).colorScheme.primary\n                : Theme.of(context).dividerColor,\n            width: _hovering ? 2 : 1,\n          ),\n          borderRadius: BorderRadius.circular(12),\n        ),\n        padding: const EdgeInsets.all(48),\n        child: const Center(child: Text('Trascina qui i tuoi file')),\n      ),\n    );\n  }\n}\n```\n\nPer il percorso inverso — aprire o salvare file con il selettore di sistema — usate `file_selector`, che è mantenuto dal team Flutter e funziona su tutte e tre le piattaforme desktop:\n\n```dart\nimport 'package:file_selector\u002Ffile_selector.dart';\n\nFuture\u003Cvoid> esportaReport(String csv) async {\n  final location = await getSaveLocation(\n    suggestedName: 'report.csv',\n    acceptedTypeGroups: const [\n      XTypeGroup(label: 'CSV', extensions: ['csv']),\n    ],\n  );\n  if (location == null) return; \u002F\u002F l'utente ha annullato\n\n  final data = XFile.fromData(\n    utf8.encode(csv),\n    mimeType: 'text\u002Fcsv',\n    name: 'report.csv',\n  );\n  await data.saveTo(location.path);\n}\n```\n\n> Attenzione su macOS: se l'app è in sandbox, dovete abilitare i permessi di lettura\u002Fscrittura nei file `macos\u002FRunner\u002FDebugProfile.entitlements` e `Release.entitlements` (`com.apple.security.files.user-selected.read-write`), altrimenti il salvataggio fallisce silenziosamente.\n\n## System tray e app che restano in background\n\nMolte utility desktop vivono nella tray e non nella taskbar. Con `tray_manager` è questione di poche righe:\n\n```dart\nimport 'package:tray_manager\u002Ftray_manager.dart';\n\nFuture\u003Cvoid> initTray() async {\n  await trayManager.setIcon(\n    Platform.isWindows ? 'assets\u002Ftray.ico' : 'assets\u002Ftray.png',\n  );\n  await trayManager.setContextMenu(Menu(items: [\n    MenuItem(key: 'show', label: 'Apri finestra'),\n    MenuItem.separator(),\n    MenuItem(key: 'exit', label: 'Esci'),\n  ]));\n}\n```\n\nCombinato con `windowManager.hide()` in `onWindowClose`, ottenete il classico comportamento \"chiudi = minimizza nella tray\".\n\n## Adattare la UI alle abitudini desktop\n\nAlcuni accorgimenti fanno la differenza tra un'app che \"sembra portata\" e una che sembra nativa:\n\n- **Hover**: su desktop il puntatore esiste. Usate `MouseRegion` e `InkWell` (che gestisce già l'hover) per dare feedback visivo. Impostate `cursor: SystemMouseCursors.click` sugli elementi cliccabili non standard.\n- **Scrollbar sempre visibili**: avvolgete le liste in `Scrollbar(thumbVisibility: true)`; su desktop la scrollbar che appare e sparisce disorienta.\n- **Target più piccoli**: i 48dp di Material pensati per il dito sono eccessivi col mouse. Impostate `visualDensity: VisualDensity.compact` (o `VisualDensity.adaptivePlatformDensity`) nel tema.\n- **Scorciatoie da tastiera**: `Shortcuts` + `Actions` sono il modo canonico; ricordate `Ctrl` su Windows\u002FLinux e `Cmd` (meta) su macOS.\n- **Tooltip**: su desktop sono attesi e gratuiti — avvolgete le `IconButton` in `Tooltip`.\n- **Navigazione**: sostituite la `BottomNavigationBar` con `NavigationRail` o un drawer permanente quando la larghezza supera i ~900px.\n\n```dart\nMaterialApp(\n  theme: ThemeData(\n    useMaterial3: true,\n    visualDensity: VisualDensity.adaptivePlatformDensity,\n    scrollbarTheme: const ScrollbarThemeData(\n      thumbVisibility: WidgetStatePropertyAll(true),\n    ),\n  ),\n  home: const HomePage(),\n);\n```\n\n### Scroll con il mouse su liste orizzontali\n\nUn dettaglio che spesso sfugge: su desktop lo scroll con trackpad orizzontale funziona, ma trascinare con il mouse su una `ListView` orizzontale no, perché di default il drag col mouse non è abilitato come input di scroll. Si risolve con uno `ScrollBehavior` personalizzato:\n\n```dart\nclass DesktopScrollBehavior extends MaterialScrollBehavior {\n  @override\n  Set\u003CPointerDeviceKind> get dragDevices => {\n        PointerDeviceKind.touch,\n        PointerDeviceKind.mouse,\n        PointerDeviceKind.trackpad,\n        PointerDeviceKind.stylus,\n      };\n}\n\n\u002F\u002F MaterialApp(scrollBehavior: DesktopScrollBehavior(), ...)\n```\n\n## Distribuzione: dal binario all'installer\n\nLa build di release produce un eseguibile, non un pacchetto installabile. Ecco il percorso tipico per ciascuna piattaforma.\n\n**Windows** — `flutter build windows --release` genera `build\u002Fwindows\u002Fx64\u002Frunner\u002FRelease\u002F`. Per la distribuzione, il pacchetto `msix` crea un installer firmato pronto per il Microsoft Store o per la distribuzione diretta:\n\n```yaml\ndev_dependencies:\n  msix: ^3.16.8\n\nmsix_config:\n  display_name: Gestionale Pro\n  publisher_display_name: Acme Srl\n  identity_name: it.acme.gestionale\n  msix_version: 1.0.0.0\n  logo_path: assets\u002Flogo.png\n  capabilities: internetClient\n```\n\n```bash\ndart run msix:create\n```\n\nIn alternativa, Inno Setup resta lo standard de facto per gli installer `.exe` classici.\n\n**macOS** — `flutter build macos --release` produce un `.app`. Per distribuirlo fuori dallo store servono firma con un Developer ID e **notarizzazione** presso Apple, altrimenti Gatekeeper blocca l'avvio:\n\n```bash\nxcrun notarytool submit MyApp.zip \\\n  --apple-id you@example.com --team-id TEAMID --wait\nxcrun stapler staple MyApp.app\n```\n\n**Linux** — `flutter build linux --release` genera un bundle in `build\u002Flinux\u002Fx64\u002Frelease\u002Fbundle\u002F`. Da lì si confeziona uno Snap (`snapcraft.yaml`), un Flatpak o un semplice `.deb`. Snap e Flatpak sono le vie più comode perché portano con sé le dipendenze GTK.\n\n## Errori frequenti da evitare\n\n1. **Assumere che il touch non esista**: molti laptop Windows hanno schermo touch. Progettate per entrambi gli input.\n2. **Ignorare il ridimensionamento estremo**: gli utenti desktop trascinano le finestre a metà schermo. Impostate sempre `minimumSize` e testate a 900×600.\n3. **Dimenticare i permessi macOS**: senza `com.apple.security.network.client` negli entitlements, le chiamate HTTP falliscono solo in release. È l'errore più segnalato di sempre.\n4. **Usare pacchetti solo mobile**: prima di aggiungere una dipendenza verificate su pub.dev i badge di piattaforma. Molti plugin popolari (es. per la biometria o le notifiche) hanno supporto desktop parziale o assente.\n5. **Non gestire le finestre multiple**: Flutter sta introducendo il supporto multi-window nel framework; nel frattempo, se serve, valutate `desktop_multi_window`.\n\n## Conclusione\n\nPortare un'app Flutter su desktop non è un `flutter build` e via: richiede di ripensare navigazione, input e ciclo di vita della finestra. La buona notizia è che l'ecosistema è maturo — `window_manager`, `file_selector`, `desktop_drop` e `tray_manager` coprono il 90% delle esigenze — e che il 95% della logica di business resta invariata.\n\nIl consiglio operativo: partite da un layout responsive già solido, aggiungete le funzionalità desktop dietro un flag come `AppPlatform.isDesktop` e mettete la firma\u002Fnotarizzazione nella pipeline CI fin dal primo giorno. È la parte più noiosa, ed è quella che tutti scoprono troppo tardi.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F93af6a59-fbf8-41f9-a358-2fa6ad0cb692.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1779452525161-61d7113d2402?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODc2MzA1MDZ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Đào Hiếu","https:\u002F\u002Funsplash.com\u002F@hieu101193","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fcomputer-desk-with-monitor-displaying-a-stylized-train-interior-rT1jjoxDNb0",null,"published","2026-08-25T04:01:46+00:00","Flutter Desktop: app native Windows, macOS e Linux","Guida pratica a Flutter Desktop: setup, window_manager, menu nativi, drag & drop di file, system tray, UI adattiva e packaging per Windows, macOS e Linux.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789120585812]