# 🛠️ How to Use Multiple Windows in Flutter Desktop

## 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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">🎥</div>
<div data-node-type="callout-text">Video version available on <a target="_blank" rel="noopener noreferrer nofollow" href="https://youtu.be/I-7ccZiPx6c" style="pointer-events: none">YouTube</a></div>
</div>

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](https://github.com/svprdga/cds_2025_04_multi-window), 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](https://pub.dev/packages/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:

```dart
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.

```dart
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.

```dart
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.

```dart
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:

```dart
@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:

```dart
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()*:

```dart
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()*:

```dart
@override
void initState() {
  super.initState();

  // Listen for messages from secondary windows
  DesktopMultiWindow.setMethodHandler(_handleMethodCall);
}
```

And here’s the handler itself:

```dart
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.

```dart
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](https://pub.dev/packages/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.
