Flutter Desktop: Navigating Multi-Window UIs with the Preview API
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
Flutter’s journey to desktop has been exciting, but one of the most requested features—true multi-window support—has felt perpetually “just around the corner.” If you’re building a desktop application that needs separate windows for documents, tools, or dashboards, you face a dilemma: do you wait, hack something together, or try to build on the unstable preview APIs? The good news is that the multi-window preview API is available now and can be used to build robust applications, provided you structure your app with the right architecture.
The core challenge isn’t just opening a new window; it’s managing shared state, communication, and lifecycle between those windows in a way that won’t break when the underlying Flutter APIs evolve. Let’s dive into how you can confidently use the preview API today while future-proofing your project.
Understanding the Multi-Window Preview API
The multi-window support is part of Flutter’s dart:ui preview APIs. You work with the PlatformDispatcher to create new windows. Each window runs its own Flutter engine and isolate, which is crucial to understand—they are separate UI clients, not just different views of the same app.
Here’s a basic example of opening a new window from your main window:
import 'dart:ui' as ui;
void openDetailsWindow(String contextId) {
final platformDispatcher = ui.PlatformDispatcher.instance;
platformDispatcher.createWindow(
WindowOptions(
title: 'Details for $contextId',
size: const Size(400, 600),
),
).then((ui.Window window) {
// This callback runs in the context of the NEW window.
runAppForWindow(window, contextId);
});
}
void runAppForWindow(ui.Window window, String contextId) {
// Attach a Flutter app to this specific window.
final FlutterView view = FlutterView(window);
runApp(DetailsView(view, contextId));
}
In your main function for the primary window, you’d run a standard runApp, but you must ensure your application can handle multiple FlutterView instances.
The Golden Rule: Externalize Shared State
The most critical architectural decision is to keep all shared state outside of the Flutter windows themselves. Treat each window as a dumb UI client that renders state and sends events. The business logic—document models, user sessions, undo history, file locks—should live in a central, window-agnostic service.
Why? This approach decouples your business logic from Flutter’s window management API. When the API changes, you only need to update the window creation and communication layers, not your core app logic.
A practical pattern is to use a simple service locator or dependency injection that can be accessed from any isolate. Since isolates don’t share memory, you need a communication bridge.
// core/app_state.dart
class DocumentRegistry {
final Map<String, Document> _openDocuments = {};
Document? getDocument(String id) => _openDocuments[id];
void openDocument(String id, Document doc) {
_openDocuments[id] = doc;
// Notify all windows (e.g., via stream controllers)
}
}
// communication/message_bus.dart
import 'package:stream_channel/stream_channel.dart';
// Using package:stream_channel for illustration.
// In reality, you might use MethodChannels or your own protocol.
class ApplicationMessageBus {
final StreamChannel<String> _channel;
ApplicationMessageBus(this._channel);
void broadcastWindowEvent(String eventType, dynamic data) {
_channel.sink.add(jsonEncode({'type': eventType, 'data': data}));
}
}
Your window’s Flutter app would then listen to this bus and send updates back to the central service.
Structuring Your Multi-Window Project
Organize your project to reflect this separation:
lib/
├── core/ # Business logic, models, services (window-agnostic)
├── services/ # State management (e.g., DocumentRegistry, UserSession)
├── communication/ # Message bus, channels for inter-window communication
├── ui/
│ ├── main_window/ # Widgets for the primary window
│ ├── detail_window/ # Widgets for secondary windows
│ └── shared/ # Common widgets
└── window_runner.dart # Bootstraps a Flutter app for a given window
Your window_runner.dart is key. It’s the entry point that each new window’s isolate executes.
// window_runner.dart
import 'dart:ui' as ui;
void main(List<String> args, ui.Window window) {
// Parse args to get a window ID or context from the main process.
final windowId = args.isNotEmpty ? args[0] : 'primary';
final flutterView = FlutterView(window);
// Connect to the shared application state via your communication layer.
final appConnection = AppConnectionService.connect(windowId);
runApp(WindowApp(
view: flutterView,
connection: appConnection,
));
}
Managing Window Lifecycle and Communication
When a window closes, you need to clean up its UI state but persist the underlying document data in your central registry. Use the Window’s callbacks:
window.onClose = () {
// Notify the central registry that this UI client is closing.
appConnection.notifyWindowClosed();
// The window and its isolate will be disposed by the system.
};
For communication, since each window is in its own isolate, you cannot simply use a global ValueNotifier. You need an inter-isolate communication solution. The dart:isolate SendPort/ReceivePort can work, but they require setup from the main process. A simpler approach for the preview phase is to use platform channels to communicate with a common native host. However, for pure Flutter, you can establish a StreamChannel between the main window’s isolate and spawned ones.
Common Pitfalls to Avoid
- Assuming Shared Memory: Remember,
staticvariables are not shared between isolates. Each window gets its own copy. - Hardcoding Window Assumptions: Don’t assume the primary window always exists. Design your state services to be resilient to any window closing.
- Over-Engineering Communication: Start with a simple event bus using
StreamChannelorRawReceivePort. You can replace it later without changing your UI or business logic. - Neglecting Platform Integration: Features like system drag-and-drop, window positioning, and menu bars often need platform-specific handling. Keep this logic in your
window_runneror communication layer.
Is It Production Ready?
The preview API is stable enough for adventurous production use if you follow the architecture outlined above. By isolating your business logic, you create a safety net. When Flutter’s stable multi-window API arrives, you’ll primarily need to adapt your window_runner and communication setup—the core of your app will remain intact.
Start by experimenting with the official Flutter multi-window example, then build your own window-agnostic state layer. This proactive approach lets you deliver a multi-window desktop app today without betting your project on API stability.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease
Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.
Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows
Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.
Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows
Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.