Desktop: macOS, Windows, Linux — Packaging & Inputs
Shipping Flutter beyond mobile — menus, shortcuts, and native packaging
Open interactive version (quiz + challenge)Real-world analogy
Think of desktop Flutter like moving from a studio apartment (mobile) to a full house — you get extra rooms (menu bar, system tray, window controls) but you also have to manage more infrastructure (packaging formats, keyboard input, multi-window state).
What is it?
Flutter desktop extends the Flutter SDK to compile native applications for macOS, Windows, and Linux, using the same widget tree but exposing platform-specific packaging, input models, and OS integration APIs.
Real-world relevance
A SaaS collaboration tool like Tixio ships a Flutter desktop client so enterprise teams can have the app live in their system tray with native keyboard shortcuts (Cmd+K search, Cmd+N new channel) and receive OS notifications even when the window is hidden.
Key points
- Desktop targets overview — Flutter supports macOS (AppKit), Windows (Win32/ANGLE), and Linux (GTK) from a single codebase. Enable with 'flutter config --enable-macos-desktop' etc. Each platform has its own plugin ecosystem and rendering path.
- When desktop Flutter makes sense — Cross-platform internal tools, Electron replacements, developer utilities, and apps already shipping on mobile where a desktop companion adds value. Avoid when heavy native platform APIs (CAMetalLayer, Win32 COM) dominate the feature set.
- Window management — package:window_manager lets you set size, min/max bounds, title, always-on-top, and listen to window lifecycle events. For multi-window, use package:desktop_multi_window. macOS needs NSPrincipalClass set in Info.plist.
- Menu bar & system tray — Use MenuBar widget (Flutter 3.7+) for native-feeling menu bars on macOS/Windows. package:tray_manager provides system tray icons with context menus — critical for background-running apps like sync clients.
- Keyboard shortcuts — Wrap the widget tree with Shortcuts + Actions or use CallbackShortcuts for simpler cases. FocusTraversalGroup controls tab order. HardwareKeyboard.instance.isKeyPressed lets you detect modifier keys for custom combos.
- Mouse & trackpad input — MouseRegion, Listener, and GestureDetector cover hover, right-click (secondary tap), scroll, and drag. Use SystemMouseCursors for cursor changes on hover. Trackpad precision scrolling is handled by PointerScrollEvent.
- Context menus — Flutter 3.3+ provides ContextMenuController and AdaptiveTextSelectionToolbar. For custom context menus on right-click, use GestureDetector onSecondaryTapDown + Overlay to position a menu at the cursor position.
- MSIX packaging (Windows) — msix package generates a signed MSIX installer for Microsoft Store or sideloading. Configure in pubspec.yaml: publisher CN, logo, capabilities. Run 'flutter pub run msix:create'. Requires Windows SDK and optionally a code-signing cert.
- DMG packaging (macOS) — flutter build macos produces a .app bundle. Wrap in a DMG with create-dmg or Xcode's Disk Utility. For Mac App Store, entitlements.plist must list sandbox capabilities. Notarisation required for distribution outside the store.
- AppImage / Snap / Flatpak (Linux) — flutter build linux produces a self-contained binary + data folder. Package as AppImage with appimagetool for portable distribution. Snap and Flatpak require manifest files but give access to software centres.
- Platform channel differences on desktop — Many mobile plugins don't have desktop implementations. Check pub.dev 'Platforms' badge. For missing plugins, write a desktop platform channel in Swift (macOS), C++ (Windows), or C (Linux) using the FFI or method-channel approach.
- Responsive layout for desktop — Use LayoutBuilder + breakpoints. Desktop users resize windows freely — test at 800px, 1200px, 1600px widths. Scaffold + NavigationRail suits medium screens; NavigationDrawer + sidebar layouts suit large. Avoid hard-coded pixel widths.
Code example
// Window setup with window_manager
import 'package:window_manager/window_manager.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
const options = WindowOptions(
size: Size(1280, 800),
minimumSize: Size(800, 600),
center: true,
title: 'Tixio Desktop',
titleBarStyle: TitleBarStyle.hidden, // custom title bar
);
await windowManager.waitUntilReadyToShow(options, () async {
await windowManager.show();
await windowManager.focus();
});
runApp(const App());
}
// Keyboard shortcut registration
class SearchShortcut extends StatelessWidget {
const SearchShortcut({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.keyK, meta: true): () =>
SearchOverlay.show(context),
const SingleActivator(LogicalKeyboardKey.keyK, control: true): () =>
SearchOverlay.show(context),
},
child: Focus(autofocus: true, child: child),
);
}
}Line-by-line walkthrough
- 1. windowManager.ensureInitialized() must be called after WidgetsFlutterBinding — it hooks into the native window before Flutter renders anything.
- 2. WindowOptions defines the initial size, minimum size, and whether to use a custom title bar (titleBarStyle: hidden removes the native chrome).
- 3. waitUntilReadyToShow ensures the window is fully configured before making it visible, preventing a flash of default-sized window.
- 4. CallbackShortcuts maps SingleActivator key combinations to callbacks — meta: true handles Cmd on macOS, control: true handles Ctrl on Windows/Linux.
- 5. Both Cmd+K and Ctrl+K are registered so the same shortcut works cross-platform without conditional logic in the business layer.
- 6. Focus(autofocus: true) ensures the widget tree receives keyboard events immediately on startup without the user clicking first.
- 7. SearchOverlay.show(context) is a static method that uses Overlay.of(context) to insert a full-screen search UI without a route push.
- 8. The pattern separates window lifecycle (main) from input handling (SearchShortcut widget) cleanly.
Spot the bug
// Attempting to show a context menu on right-click
GestureDetector(
onSecondaryTap: (details) {
showMenu(
context: context,
position: RelativeRect.fill,
items: [
PopupMenuItem(child: Text('Copy')),
PopupMenuItem(child: Text('Delete')),
],
);
},
child: const ListTile(title: Text('Item')),
)Need a hint?
The menu appears but in the wrong position. What's wrong with how position is calculated?
Show answer
Bug: RelativeRect.fill positions the menu at the edges of the screen, not at the cursor. Fix: use the tap details to get the global position. Use onSecondaryTapDown instead of onSecondaryTap (which doesn't provide position), then compute: final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; and position: RelativeRect.fromRect(details.globalPosition & const Size(1,1), Offset.zero & overlay.size). This places the menu exactly where the user right-clicked.
Explain like I'm 5
Mobile apps are like phones — everyone carries one the same way. Desktop apps are like computers — people use mice and keyboards and want menus at the top of the screen. Flutter can make both, but you have to learn the computer rules: how to package it for Windows or Mac, how to handle right-clicks, and how to add a little icon in the corner of the screen.
Fun fact
Flutter desktop was originally built to power Google's internal tooling at scale before being released publicly — meaning production-grade internal apps validated the approach before the community got access.
Hands-on challenge
Build a minimal Flutter macOS app with: (1) a hidden title bar using window_manager, (2) a Cmd+K shortcut that shows a search overlay, (3) a system tray icon with 'Show' and 'Quit' menu items, and (4) a minimum window size of 800x600.
More resources
- Flutter Desktop Support (Flutter Docs)
- window_manager package (pub.dev)
- MSIX packaging for Flutter (pub.dev)
- Building Desktop Apps with Flutter (Flutter Medium)