Flutter desktop: building for macOS and Windows
By Ann Tech · 24 December 2025
Flutter's desktop support left beta in Flutter 3.0. macOS and Windows apps built with Flutter share virtually all business logic and UI with your mobile app — but desktop introduces constraints and interactions that mobile doesn't have: window resizing, keyboard navigation, right-click context menus, and file system access.
Enabling desktop targets
flutter config --enable-macos-desktop
flutter config --enable-windows-desktop
# Create or add desktop support to an existing project
flutter create --platforms=macos,windows .
This adds macos/ and windows/ directories alongside android/ and ios/.
Responsive layouts for desktop
Mobile layouts assume a narrow portrait screen. Desktop windows can be 1440px wide and resized at any time. Use LayoutBuilder to adapt:
class AdaptiveLayout extends StatelessWidget {
const AdaptiveLayout({super.key, required this.body});
final Widget body;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200) {
return _WideLayout(body: body);
} else if (constraints.maxWidth >= 600) {
return _MediumLayout(body: body);
} else {
return _NarrowLayout(body: body);
}
},
);
}
}
class _WideLayout extends StatelessWidget {
const _WideLayout({required this.body});
final Widget body;
@override
Widget build(BuildContext context) {
return Row(
children: [
const SizedBox(width: 260, child: AppSidebar()),
Expanded(child: body),
],
);
}
}
For desktop, a sidebar navigation (NavigationRail or custom) replaces the BottomNavigationBar.
Keyboard shortcuts
Desktop users expect keyboard shortcuts. Wire them with Shortcuts and Actions:
class AppShortcuts extends StatelessWidget {
const AppShortcuts({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyN):
const NewDocumentIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
const SaveIntent(),
LogicalKeySet(LogicalKeyboardKey.escape):
const DismissIntent(),
},
child: Actions(
actions: {
NewDocumentIntent: CallbackAction<NewDocumentIntent>(
onInvoke: (_) => context.read<DocumentBloc>().add(const NewDocument()),
),
SaveIntent: CallbackAction<SaveIntent>(
onInvoke: (_) => context.read<DocumentBloc>().add(const SaveDocument()),
),
},
child: Focus(autofocus: true, child: child),
),
);
}
}
Right-click context menus
class ContextMenuWrapper extends StatelessWidget {
const ContextMenuWrapper({super.key, required this.child, required this.items});
final Widget child;
final List<ContextMenuEntry> items;
@override
Widget build(BuildContext context) {
return GestureDetector(
onSecondaryTapDown: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 1,
details.globalPosition.dy + 1,
),
items: items.map((e) => PopupMenuItem(
onTap: e.onTap,
child: Row(
children: [
Icon(e.icon, size: 16),
const SizedBox(width: 8),
Text(e.label),
],
),
)).toList(),
);
},
child: child,
);
}
}
File system access
For file open/save dialogs, use file_picker:
dependencies:
file_picker: ^8.0.0
Future<void> openFile() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json', 'yaml'],
);
if (result == null) return;
final path = result.files.single.path!;
final content = await File(path).readAsString();
// process content
}
Future<void> saveFile(String content) async {
final path = await FilePicker.platform.saveFile(
dialogTitle: 'Save file',
fileName: 'export.json',
);
if (path == null) return;
await File(path).writeAsString(content);
}
Window management
Control the window size and title with window_manager:
dependencies:
window_manager: ^0.3.0
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
const options = WindowOptions(
size: Size(1200, 800),
minimumSize: Size(800, 600),
center: true,
title: 'My Desktop App',
titleBarStyle: TitleBarStyle.hidden, // Custom title bar
);
await windowManager.waitUntilReadyToShow(options, () async {
await windowManager.show();
await windowManager.focus();
});
runApp(const MyApp());
}
Platform-specific code
Detect the platform and adapt:
import 'dart:io';
Widget buildNavigation(Widget body) {
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
return Row(
children: [const Sidebar(), Expanded(child: body)],
);
}
return Scaffold(
body: body,
bottomNavigationBar: const AppBottomNav(),
);
}
Common pitfalls
Touch-only interactions. Drag-to-dismiss drawers, swipe-to-delete, and long-press context menus don't work well with a mouse. Add hover states, right-click menus, and keyboard alternatives.
Small tap targets. Mobile buttons sized for fingers (48dp) look oversized on desktop. Adapt sizes: isDesktop ? 32.0 : 48.0.
Missing scrollbar. Desktop users expect visible scrollbars. Wrap ListView with Scrollbar(thumbVisibility: true, ...).
No window restore. Save window size and position to shared_preferences and restore on next launch — desktop users expect this.
macOS entitlements. macOS apps run in a sandbox. File access, network requests, and camera use require explicit entitlements in macos/Runner/DebugProfile.entitlements and ReleaseProfile.entitlements.
Sign in to like, dislike, or report.