Introduction
One of Flutter’s key strengths is its ability to run on virtually all major platforms today. And while I often focus on mobile and web examples here on this blog, it’s important not to overlook the fact that Flutter is also a perfectly viable option for Windows, macOS, and Linux.
In fact, the latest major release, Flutter 3.32, introduced some quality-of-life improvements that are definitely worth exploring. So today, we’re going to take a look at working with multiple windows in Flutter desktop.
đź“˝ Video version available on YouTube
This won’t be a traditional tutorial. I’m trying out a more relaxed format where I’ll walk you through small but essential code snippets that show how to work with multiple windows. That said, if you’re looking for a more step-by-step guide, don’t worry, here you can access the complete source code, so you can run it and study it in detail.
The desktop_multi_window plugin
Before we dive into the implementation, let’s take a moment to understand how Flutter currently handles multiple windows on desktop.
Out of the box, Flutter doesn’t offer a fully abstracted or official solution for managing multiple windows. If you want to do it manually, you will need to write platform-specific code: creating new native windows and manually instantiating a second FlutterEngine for each one. It’s not exactly beginner-friendly, and definitely not ideal for cross-platform apps trying to stay within Dart code as much as possible.
That’s where the desktop_multi_window plugin comes in. This package wraps all of that complexity and gives us a clean API for creating and managing windows on Linux, macOS, and Windows, without having to dive into C++, Objective-C, or Windows APIs.
Under the hood, though, each of those windows runs its own separate Flutter engine, and they still rely on native code to communicate with each other. That’s why we use Flutter’s familiar MethodChannel system to pass messages between them. So, throughout the video, you’ll see things like MethodCall, invokeMethod, and similar patterns being used to send data back and forth.
Hopefully, the Flutter team will eventually introduce a more official and streamlined way to handle multi-window setups directly from Dart. But until then, this plugin is quite mature and works well for most practical use cases.
Create a class that represents a window
The first thing I recommend when working with multiple windows is to create a class that holds the properties related to each individual window. In my sample project, I created a class called WindowInfo, and here’s its basic structure:
class WindowInfo {
final int id;
final String name;
final WindowController controller;
WindowInfo({
required this.id,
required this.name,
required this.controller,
});
}
At the very least, you’ll want to store the window’s id and a WindowController, which will let you later close it, bring it to focus, or perform any other actions. I also added a name field to make identification easier. Depending on the kind of app you’re building, you can extend this class with any additional data you find useful to better manage your open windows.
In my project, I keep an array of these objects in memory so I can easily work with them later on.
Creating Windows
Let’s start by looking at how we can create new secondary windows from a main window.
Future<void> _createWindow() async {
try {
// Set window details
final name = 'Window $_windowCounter';
final windowConfig = {
'name': name,
};
// Create the new window
final windowController = await DesktopMultiWindow.createWindow(
jsonEncode(windowConfig),
);
// Configure the window using the controller
windowController
..setFrame(const Offset(100, 100) & const Size(800, 600))
..setTitle(name)
..show();
// Add to our tracking list
setState(() {
_windows.add(
WindowInfo(
id: windowController.windowId,
name: name,
controller: windowController,
),
);
_windowCounter++;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to create window: $e')));
}
}
}
We begin by wrapping the whole operation in a try-catch block. That’s because anything related to window management can potentially fai, so better to be safe.
We’re going to prepare the data for the new window. Since we’re ultimately working with the native layer, DesktopMultiWindow relies heavily on plain JSON strings.
So first, we store the window’s name in a variable, and then we create a map that includes a "name" property.
Then we call DesktopMultiWindow.createWindow(), which launches the new window. This returns a WindowController object, which we can use to interact with the window later on.
Next, we configure some properties, position, size, title, and finally call show() to display it.
The final part of the method stores the window’s data in an array, so we can interact with it later. In my example, I’m using setState() just to keep things simple, but I recommend using your favorite state management solution in a real-world project.
Closing windows
Now let’s take a look at how we can close a secondary window when it’s no longer needed.
Future<void> _closeWindow(int windowId) async {
try {
// Find the window controller for this window ID
final windowInfo = _windows.firstWhere((w) => w.id == windowId);
// Use the controller's close method
await windowInfo.controller.close();
setState(() {
_windows.removeWhere((w) => w.id == windowId);
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to close window: $e')));
}
}
}
We start with a try-catch block, just like we did when creating windows. Then we look for the WindowInfo object that matches the window ID we want to close. This gives us access to the corresponding controller.
Once we have that, we simply call its close() method. That’s all it takes to close the actual native window.
After closing it, we also want to keep our internal list of windows clean, so we remove the corresponding WindowInfo from the array.
And finally, if anything goes wrong during the process, we show a quick message to the user using a SnackBar, or whatever error handling method you prefer.
Send messages to a secondary window
Let’s take a look at how to send messages from the main window to a secondary one.
Future<void> _sendMessageToWindow(int windowId) async {
try {
final response = await DesktopMultiWindow.invokeMethod(
windowId,
'message_from_main',
'Hello from main window!',
);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Response: $response')));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to send message: $e')));
}
}
}
We start by using DesktopMultiWindow.invokeMethod(), which allows us to send a message directly to another window by its ID.
The first argument is the ID of the target window. The second is the method name, in this case, "message_from_main". And the third is the data we want to send, which is just a string message.
This method returns a response from the secondary window, if any. In this example, we display that response using a SnackBar, but you could use it however you like.
To receive messages in the secondary window, we first register a method handler during the initState() of our widget:
@override
void initState() {
super.initState();
// Listen for messages from the main window
DesktopMultiWindow.setMethodHandler(_handleMethodCall);
}
This sets up the method that will handle incoming calls from other windows, typically from the main window.
Now here’s what the actual handler method looks like:
Future<dynamic> _handleMethodCall(MethodCall call, int fromWindowId) async {
if (call.method == 'message_from_main') {
final message = call.arguments.toString();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
return 'Message received by secondary window ${widget.windowId}';
}
return null;
}
Inside this function, we check if the method is "message_from_main". If it is, we extract the message, display it using a SnackBar, and return a response indicating that the message was received.
This is the response that the main window gets back when it calls invokeMethod().
This simple setup lets each secondary window listen for commands or data from the main window, and respond if needed. And of course, you can add as many method types as your app requires.
Send messages to the main window from a secondary window
We have already seen how the main window can send messages to secondary ones. Now let’s quickly complete the picture by allowing secondary windows to send messages back to the main window.
This is all done in the same way, using DesktopMultiWindow.invokeMethod():
await DesktopMultiWindow.invokeMethod(
0, // 0 is usually the ID of the main window
'message_from_secondary',
'Hello from window ${widget.windowId}',
);
The only difference here is that we’re targeting window ID 0, which represents the main window.
On the main window side, we handle this message with a method handler registered during initState():
@override
void initState() {
super.initState();
// Listen for messages from secondary windows
DesktopMultiWindow.setMethodHandler(_handleMethodCall);
}
And here’s the handler itself:
Future<dynamic> _handleMethodCall(MethodCall call, int fromWindowId) async {
if (call.method == 'message_from_secondary') {
final message = call.arguments.toString();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
return 'Message received by main window';
}
return null;
}
With this, you now have full two-way communication between windows using simple method calls and arguments.
Focusing a window
Let’s wrap up this overview by showing how to bring a specific window to the front.
Future<void> _focusWindow(int windowId) async {
try {
// Find the window controller for this window ID
final windowInfo = _windows.firstWhere((w) => w.id == windowId);
// Use the controller's show method to bring window to front
await windowInfo.controller.show();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to focus window: $e')));
}
}
}
First, we search for the WindowInfo object that matches the ID of the window we want to focus. That gives us access to its controller.
Then we simply call show() on the controller. Internally, this brings the window to the foreground if it’s already open. It’s an easy way to programmatically focus or activate any window you’ve previously created.
And that’s all it takes.
Conclusion
Today we explored how to manage multiple windows in a Flutter desktop app using the desktop_multi_window plugin. We looked at how to create and close windows, how to send messages back and forth between them, and how to bring any window into focus when needed.
Hopefully this gave you a solid overview of how multi-window support works in Flutter and how to integrate it into your own projects.
Thank you very much for reading this article to the end. I hope you have a wonderful rest of your day, goodbye.
Top comments (0)