Lesson 51 of 77 intermediate

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

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. 1. windowManager.ensureInitialized() must be called after WidgetsFlutterBinding — it hooks into the native window before Flutter renders anything.
  2. 2. WindowOptions defines the initial size, minimum size, and whether to use a custom title bar (titleBarStyle: hidden removes the native chrome).
  3. 3. waitUntilReadyToShow ensures the window is fully configured before making it visible, preventing a flash of default-sized window.
  4. 4. CallbackShortcuts maps SingleActivator key combinations to callbacks — meta: true handles Cmd on macOS, control: true handles Ctrl on Windows/Linux.
  5. 5. Both Cmd+K and Ctrl+K are registered so the same shortcut works cross-platform without conditional logic in the business layer.
  6. 6. Focus(autofocus: true) ensures the widget tree receives keyboard events immediately on startup without the user clicking first.
  7. 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. 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

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery