Today we are breaking out of the single-window mobile constraint and building a true multi-window desktop app in Flutter using the new experimental Desktop Windowing API.
What you'll learn
- How to enable and configure the native windowing experimental channel in Flutter.
- Managing window controllers for regular, popup, and dialog windows.
- Nesting window hierarchies while sharing app state seamlessly.
- Handling window lifecycle events with custom controllers and delegates.
Enabling the Windowing API
Flutter grew up as a mobile framework where apps take up the entire screen and modals are just stacked UI overlays. Desktop operating systems demand real multi-window capabilities.
To start drawing outside a single canvas, switch your Flutter channel and enable the experimental desktop flag:
// Run these commands in your terminal to set up your environment
// flutter channel main
// flutter upgrade
// flutter config --enable-windowing
Speed Note: These experimental APIs change fast. Make sure your local Flutter SDK is pinned to the exact main branch commit where you tested your build to avoid unexpected breaking changes in window controller signatures.
Creating and Controlling Native Windows
Unlike mobile overlays, desktop windows need explicit orchestration via platform channels. We use a WindowController to initialize properties like size, title, and positioning before mounting the native handles.
Here is how you instantiate and manipulate a standard top-level application window:
import 'package:flutter/widgets.dart';
final controller = WindowController(
title: 'Production Workspace',
size: const Size(1200, 800),
);
// Later in your logic, update dimensions or close the instance cleanly
void updateWindowLayout() {
controller.setTitle('Workspace [Modified]');
controller.setSize(const Size(1400, 900));
}
Gotcha: Platforms like macOS or Windows may constrain or outright ignore your requested dimensions if they fall outside min/max system constraints or available display bounds. Always design your widgets to handle responsive resizing gracefully.
Structuring Window Hierarchies and Types
Desktop apps rely on specialized window types—popups, tooltips, dialogs, and satellites—that behave differently regarding focus and stacking order.
When you need a modal confirmation dialog that blocks user interaction with the main workspace, instantiate a DialogWindowController tied to a parent controller:
final parentController = WindowController(title: 'Main App', size: const Size(800, 600));
final dialogController = DialogWindowController(
title: 'Confirm Deletion',
size: const Size(400, 200),
parent: parentController,
);
Widget buildDialogWindow(BuildContext context) {
return DialogWindow(
controller: dialogController,
child: const Center(
child: Text('Are you sure you want to delete this resource?'),
),
);
}
Accessibility Note: Popup and dialog windows automatically receive keyboard input focus. Ensure your child components handle standard tab-traversing and escape-key dismissals so keyboard-only users don't get trapped in child loops.
Rendering Widgets Across Multiple Windows
A major advantage of this API design is that all active native windows exist inside the exact same Flutter widget tree. You do not need multiple engine instances or complex inter-process communication bridges.
State management libraries like Riverpod, Provider, or BLoC work out of the box across all open windows:
class MainWindowRoot extends StatelessWidget {
const MainWindowRoot({super.key, required this.controller});
final WindowController controller;
@override
Widget build(BuildContext context) {
return Window(
controller: controller,
child: Scaffold(
appBar: AppBar(title: const Text('Shared State App')),
body: Center(
child: FilledButton(
onPressed: () {
// Open a secondary window sharing the same widget tree context
openSettingsWindow();
},
child: const Text('Open Settings Window'),
),
),
),
);
}
}
Listening to Window Events and Lifecycle Hooks
Desktop apps need to react when users click the native close button or attempt to destroy a window. We implement this using a WindowControllerDelegate combined with WindowScope inheritance.
Here is how you intercept a close request to prompt the user or exit the process cleanly:
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class DesktopLifecycleDelegate with WindowControllerDelegate {
@override
void onWindowCloseRequested(WindowController controller) {
// Perform cleanup or save state before closing
ServicesBinding.instance.exitApplication(AppExitType.required);
}
}
final primaryController = WindowController(
title: 'Dashboard',
size: const Size(1024, 768),
delegate: DesktopLifecycleDelegate(),
);
Gotcha: If you fail to supply a custom
WindowControllerDelegateon your primary application window, closing the native OS frame will orphan the Flutter engine process in the background, leaving a zombie process running in your task manager.
Summary
Moving Flutter to desktop environments requires moving past single-canvas mobile assumptions. By leveraging WindowController, window hierarchies, and unified widget trees, you can ship professional multi-window desktop applications with zero boilerplate state duplication.
Next Steps
- Try building a secondary floating inspector window that updates its position automatically relative to a parent workspace.
- Implement a
WindowScope.titleOf(context)listener to dynamically update your internal widget headers when native window titles change. - Test your multi-window layout on both Windows and macOS to verify native window manager integration behavior.