MultiWindowManager
Flutter desktop plugin for creating and managing multiple windows: resizing, repositioning, and inter-window communication.
Fork and re-work of window_manager_plus, which itself is based on window_manager. Key additions: window reuse cache (avoid re-creating Flutter engines), cross-window registry backed by shared native state, fixed critical errors fixed restart and crash after close secondary window, fast isolate-isolate channel fast channel for realtime windows notify - ipc.
Linux is not currently supported.
Platform Support
| Linux | macOS | Windows |
|---|---|---|
| n/a | + | + |
Setup
Windows setup
Edit windows/runner/main.cpp:
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
+#include <iostream>
#include "flutter_window.h"
#include "utils.h"
+#include "multi_window_manager/multi_window_manager_plugin.h"
int APIENTRY wWinMain(...) {
...
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.CreateAndShow(L"my_app", origin, size)) {
return EXIT_FAILURE;
}
- window.SetQuitOnClose(true);
+ window.SetQuitOnClose(false); // let MultiWindowManager decide when to quit
+ MultiWindowManagerPluginSetWindowCreatedCallback(
+ [](std::vector<std::string> command_line_arguments) {
+ flutter::DartProject project(L"data");
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ auto window = std::make_shared<FlutterWindow>(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window->CreateAndShow(L"my_app", origin, size)) {
+ std::cerr << "Failed to create window" << std::endl;
+ }
+ window->SetQuitOnClose(false);
+ return std::move(window);
+ });
::MSG msg;
...
SetQuitOnClose(false) prevents each child window from terminating the process when it closes.
macOS setup
Edit macos/Runner/MainFlutterWindow.swift:
import Cocoa
import FlutterMacOS
+import multi_window_manager
class MainFlutterWindow: NSPanel {
override func awakeFromNib() {
...
RegisterGeneratedPlugins(registry: flutterViewController)
+ MultiWindowManagerPlugin.RegisterGeneratedPlugins = RegisterGeneratedPlugins
super.awakeFromNib()
}
+ override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
+ super.order(place, relativeTo: otherWin)
+ hiddenWindowAtLaunch()
+ }
}
Edit macos/Runner/AppDelegate.swift:
import Cocoa
import FlutterMacOS
+import multi_window_manager
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
- return true
+ // Close the app only when windows not contains MainFlutterWindow
+ return !NSApp.windows.contains(where: { $0 is MainFlutterWindow })
}
}
Usage
Initialization
Call ensureInitialized once before accessing MultiWindowManager.current.
The args list passed to main() carries the window ID in args[0] (absent for the main window).
import 'package:flutter/material.dart';
import 'package:multi_window_manager/multi_window_manager.dart';
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
final windowId = args.isEmpty ? 0 : int.tryParse(args[0]) ?? 0;
if (windowId == 0) {
// Main window - never participates in the reuse cache.
await MultiWindowManager.ensureInitialized(windowId);
} else {
// Secondary window - optionally enable reuse cache.
await MultiWindowManager.ensureInitializedSecondary(
windowId,
isEnabledReuse: true, // hide instead of destroy on close
);
}
WindowOptions windowOptions = const WindowOptions(
size: Size(800, 600),
center: true,
backgroundColor: Colors.transparent,
titleBarStyle: TitleBarStyle.hidden,
);
MultiWindowManager.current.waitUntilReadyToShow(windowOptions, () async {
await MultiWindowManager.current.show();
await MultiWindowManager.current.focus();
});
runApp(const MyApp());
}
Create a window
// Spawn a new Flutter engine window, passing optional string arguments.
final newWindow = await MultiWindowManager.createWindow(['type=dashboard', 'userId=42']);
if (newWindow != null) {
print('Created window ${newWindow.id}');
}
The arguments are available in main(List<String> args) of the new window as args[1], args[2], etc.
Reuse cached windows
createWindowOrReuse avoids spawning a new Flutter engine when a previously-hidden reuse-enabled window is available. Claiming a hidden window is atomic - concurrent calls from different windows cannot pick the same target.
// Any window can call this - main or secondary.
final window = await MultiWindowManager.createWindowOrReuse(
args: ['type=dashboard', 'userId=42'],
);
For a window to participate in the reuse cache:
- Initialize it with
isEnabledReuse: true(see Initialization). - Use
ReuseWindowas the root widget:
// Secondary window entry-point:
runApp(MaterialApp(
home: ReuseWindow(
initialArgs: parsedArgs,
windowOptions: const WindowOptions(
size: Size(1280, 720),
center: true,
titleBarStyle: TitleBarStyle.hidden,
),
loadingBuilder: (context) => const Center(child: CircularProgressIndicator()),
builder: (context, args) {
// Called on first show AND on every reuse with the latest args.
return MyPage(args: args);
},
),
));
When the user closes a ReuseWindow, the native layer hides the OS window instead of destroying the Flutter engine. createWindowOrReuse can later reclaim it with new args, rebuilding the content without the overhead of creating a new engine.
setPreventClose works independently from the reuse mechanism, so inner widgets can still show "are you sure?" dialogs without breaking reuse.
Communication between windows
// Send an event from any window to window with id 1.
final result = await MultiWindowManager.current.invokeMethodToWindow(
1,
'showNotification',
{'message': 'Hello from window ${MultiWindowManager.current.id}'},
);
// In window 1 - implement WindowListener.onEventFromWindow:
class _MyWidgetState extends State<MyWidget> with WindowListener {
@override
void initState() {
super.initState();
MultiWindowManager.current.addListener(this);
}
@override
void dispose() {
MultiWindowManager.current.removeListener(this);
super.dispose();
}
@override
Future<dynamic> onEventFromWindow(
String eventName, int fromWindowId, dynamic arguments) async {
if (eventName == 'showNotification') {
print('Message from $fromWindowId: ${arguments['message']}');
return 'acknowledged';
}
return null;
}
}
To get all registered window IDs:
final ids = await MultiWindowManager.getAllWindowManagerIds();
High-frequency IPC (WindowIpc)
WindowIpc is a persistent channel built on Dart's IsolateNameServer. After a one-time native handshake, all data flows through SendPort / ReceivePort with no native layer involvement. This makes it suitable for animation-rate state sync, ChangeNotifier updates, and any scenario where invokeMethodToWindow would be called too frequently.
The channel is a lazy singleton on each window - no setup or disposal required:
final ipc = MultiWindowManager.current.ipc;
Wire format: a flat List<Object?>. List index access has no hashing overhead compared to Map, which matters at high message rates.
Connect
One call establishes a bidirectional link. Both windows can send and receive after a single handshake:
// Window A: connect to window B.
await MultiWindowManager.current.ipc.connectWindow(windowBId);
// Window B: react when A connects.
MultiWindowManager.current.ipc.onConnected = (int fromId) {
// Set up stream listeners here.
};
// Either side: react when the peer closes or hides.
MultiWindowManager.current.ipc.onDisconnected = (int fromId) {
// Clean up listeners here.
};
Send and receive raw payloads
// Sender: fire-and-forget, no await needed.
await MultiWindowManager.current.ipc.notifyWindow(targetId, ['counter', 1, 'ts', timestamp]);
// Receiver:
MultiWindowManager.current.ipc.listenWindow(sourceId).listen((payload) {
final counter = payload[1] as int;
});
Use a topic string at index 0 to route messages between multiple features sharing the same channel:
ipc.notifyWindow(targetId, ['metrics', 'fps', 60, 'mem', 128]);
ipc.notifyWindow(targetId, ['input', 'x', 0.5, 'y', 0.3]);
ipc.listenWindow(sourceId)
.where((m) => m[0] == 'metrics')
.listen((m) => /* ... */);
Sync a ChangeNotifier
Use IpcNotifierSender on the source window and IpcNotifierReceiver on the target window.
Multiple fields changed in the same synchronous call stack are coalesced into one message via scheduleMicrotask, so rapid successive assignments produce a single IPC event instead of one per field.
Source window:
class PersonNotifier extends ChangeNotifier with IpcNotifierSender {
String _name = '';
int _age = 0;
String get name => _name;
set name(String v) {
_name = v;
notifyListeners();
ipcMark('name', v); // queues field for next microtask flush
}
int get age => _age;
set age(int v) {
_age = v;
notifyListeners();
ipcMark('age', v);
}
}
// Setup (once, after ensureInitialized):
final notifier = PersonNotifier();
notifier.ipcSetup(MultiWindowManager.current.ipc, 'person');
// Both fields changed synchronously = ONE IPC message.
notifier.name = 'Alice';
notifier.age = 30;
Target window:
class PersonNotifier extends ChangeNotifier with IpcNotifierReceiver {
String name = '';
int age = 0;
@override
void ipcApplyField(String field, Object? value) {
switch (field) {
case 'name': name = value as String;
case 'age': age = value as int;
}
// notifyListeners() is called once after the full batch.
}
@override
void dispose() {
ipcCancelListeners();
super.dispose();
}
}
// Setup: call inside ipc.onConnected so the receiver starts when the
// source window connects (not before).
MultiWindowManager.current.ipc.onConnected = (int fromId) {
myNotifier.ipcListen(MultiWindowManager.current.ipc, fromId, 'person');
};
Multiple notifiers can share the same channel by using different topic strings:
personNotifier.ipcSetup(ipc, 'person');
productNotifier.ipcSetup(ipc, 'product');
personNotifier.ipcListen(ipc, sourceId, 'person');
productNotifier.ipcListen(ipc, sourceId, 'product');
Performance comparison
| Approach | 1 000 messages sent | Receive throughput | Notes |
|---|---|---|---|
WindowIpc (fire-and-forget) |
~15 ms | ~66 000 msg/s | No native layer after handshake |
invokeMethodToWindow (no await) |
~15 ms | ~20 000 msg/s | Platform thread is bottleneck |
invokeMethodToWindow (await each) |
~1 600 ms | ~625 msg/s | Full round-trip per call |
Window events
class _MyWidgetState extends State<MyWidget> with WindowListener {
@override
void initState() {
super.initState();
MultiWindowManager.current.addListener(this);
}
@override
void dispose() {
MultiWindowManager.current.removeListener(this);
super.dispose();
}
@override
void onWindowFocus([int? windowId]) => setState(() {});
@override
void onWindowClose([int? windowId]) { /* ... */ }
@override
void onWindowMaximize([int? windowId]) { /* ... */ }
@override
void onWindowMinimize([int? windowId]) { /* ... */ }
// Catches every event by name, including reuse events.
@override
void onWindowEvent(String eventName, [int? windowId]) {
print('event: $eventName window: $windowId');
}
}
For global listeners (receive events from all windows with windowId set):
MultiWindowManager.addGlobalListener(myGlobalListener);
MultiWindowManager.removeGlobalListener(myGlobalListener);
Window registry
MultiWindowManager.registry is a per-isolate reactive view backed by the shared native state. Use it to drive UI that reflects the live window list.
// In a widget:
ValueListenableBuilder<List<int>>(
valueListenable: MultiWindowManager.registry.activeWindows,
builder: (context, ids, _) => Text('Open windows: $ids'),
);
ValueListenableBuilder<List<int>>(
valueListenable: MultiWindowManager.registry.hiddenWindows,
builder: (context, ids, _) => Text('Cached (reusable) windows: $ids'),
);
For a guaranteed-fresh read bypassing the local cache:
final activeIds = await MultiWindowManager.registry.getActiveWindowIds();
final hiddenIds = await MultiWindowManager.registry.getHiddenWindowIds();
Confirm before closing
class _MyWidgetState extends State<MyWidget> with WindowListener {
@override
void initState() {
super.initState();
MultiWindowManager.current.addListener(this);
MultiWindowManager.current.setPreventClose(true);
}
@override
void dispose() {
MultiWindowManager.current.removeListener(this);
super.dispose();
}
@override
void onWindowClose([int? windowId]) async {
if(!await MultiWindowManager.current.isPreventClose()) return;
final confirmed = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Close window?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Close'),
),
],
),
);
if (confirmed == true) {
await MultiWindowManager.current.setPreventClose(false);
await MultiWindowManager.current.close();
}
}
}
Quit on close
macOS
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
- return true
+ return false
}
Windows
Set SetQuitOnClose(false) for every window (see Windows setup).
To exit when the main window closes, handle onWindowClose and call exit(0).
Hidden at launch
Windows
Edit windows/runner/win32_window.cpp to create the window without WS_VISIBLE:
HWND window = CreateWindow(
- window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
...);
Edit windows/runner/flutter_window.cpp to remove the auto-show:
flutter_controller_->engine()->SetNextFrameCallback([&]() {
- this->Show();
});
API
MultiWindowManager
Instance methods
addListener(WindowListener listener) -> void
Subscribe to window events for this window.
removeListener(WindowListener listener) -> void
Unsubscribe from window events.
show({bool inactive = false}) -> Future<void>
Shows and gives focus to the window.
hide() -> Future<void>
Hides the window.
close() -> Future<void>
Try to close the window (respects setPreventClose).
destroy() -> Future<void>
Force-close app, bypassing setPreventClose.
focus() -> Future<void>
Focuses on the window.
blur() -> Future<void>
Removes focus from the window.
isFocused() -> Future<bool>
Returns whether the window is currently focused.
center({bool animate = false}) -> Future<void>
Moves the window to the center of the screen.
setPosition(Offset position, {bool animate = false}) -> Future<void>
Moves the window to a position.
getPosition() -> Future<Offset>
Returns the current window position.
setAlignment(Alignment alignment, {bool animate = false}) -> Future<void>
Moves the window to a screen-aligned position.
setSize(Size size, {bool animate = false}) -> Future<void>
Resizes the window.
getSize() -> Future<Size>
Returns the current window size.
setBounds(Rect? bounds, {Offset? position, Size? size, bool animate = false}) -> Future<void>
Resizes and moves the window to the supplied bounds.
getBounds() -> Future<Rect>
Returns the window bounds as a Rect.
setMinimumSize(Size size) -> Future<void>
Sets the minimum window size.
setMaximumSize(Size size) -> Future<void>
Sets the maximum window size.
maximize({bool vertically = false}) -> Future<void>
Maximizes the window. vertically simulates aero snap (Windows only).
unmaximize() -> Future<void>
Unmaximizes the window.
isMaximized() -> Future<bool>
Returns whether the window is maximized.
isMaximizable() -> Future<bool>
Returns whether the window can be maximized by the user.
setMaximizable(bool isMaximizable) -> Future<void>
Sets whether the window can be maximized by the user.
minimize() -> Future<void>
Minimizes the window.
restore() -> Future<void>
Restores the window from a minimized state.
isMinimized() -> Future<bool>
Returns whether the window is minimized.
isMinimizable() -> Future<bool>
Returns whether the window can be minimized by the user.
setMinimizable(bool isMinimizable) -> Future<void>
Sets whether the window can be minimized by the user.
setFullScreen(bool isFullScreen) -> Future<void>
Sets whether the window should be in full-screen mode.
isFullScreen() -> Future<bool>
Returns whether the window is in full-screen mode.
isVisible() -> Future<bool>
Returns whether the window is visible to the user.
setTitle(String title) -> Future<void>
Changes the title of the native window.
getTitle() -> Future<String>
Returns the title of the native window.
setTitleBarStyle(TitleBarStyle titleBarStyle, {bool windowButtonVisibility = true}) -> Future<void>
Changes the title bar style of the native window.
getTitleBarHeight() -> Future<int>
Returns the title bar height in logical pixels.
setAlwaysOnTop(bool isAlwaysOnTop) -> Future<void>
Sets whether the window is always on top of other windows.
isAlwaysOnTop() -> Future<bool>
Returns whether the window is always on top.
setAlwaysOnBottom(bool isAlwaysOnBottom) -> Future<void>
Sets whether the window is always below other windows.
isAlwaysOnBottom() -> Future<bool>
Returns whether the window is always on the bottom.
setOpacity(double opacity) -> Future<void>
Sets the window opacity (0.0 fully transparent, 1.0 fully opaque).
getOpacity() -> Future<double>
Returns the current window opacity.
setBackgroundColor(Color backgroundColor) -> Future<void>
Sets the background color of the window.
setPreventClose(bool isPreventClose) -> Future<void>
Intercepts the native close signal. Use with onWindowClose to show a confirmation dialog.
isPreventClose() -> Future<bool>
Returns whether the native close signal is being intercepted.
setResizable(bool isResizable) -> Future<void>
Sets whether the window can be resized by the user.
isResizable() -> Future<bool>
Returns whether the window is resizable.
setMovable(bool isMovable) -> Future<void>
Sets whether the window can be moved by the user.
isMovable() -> Future<bool>
Returns whether the window is movable.
setClosable(bool isClosable) -> Future<void>
Sets whether the window can be manually closed by the user.
isClosable() -> Future<bool>
Returns whether the window can be closed by the user.
setSkipTaskbar(bool isSkipTaskbar) -> Future<void>
Makes the window not show in the taskbar / dock.
isSkipTaskbar() -> Future<bool>
Returns whether the window is hidden from the taskbar.
setHasShadow(bool hasShadow) -> Future<void>
Sets whether the window has a shadow (frameless windows only on Windows).
hasShadow() -> Future<bool>
Returns whether the window has a shadow.
setProgressBar(double progress) -> Future<void>
Sets the taskbar progress bar value (0.0 to 1.0). Windows only.
setIcon(String iconPath) -> Future<void>
Sets the window / taskbar icon.
dock({required DockSide side, required int width}) -> Future<void>
Docks the window to a screen edge. Windows only.
undock() -> Future<bool>
Undocks the window. Windows only.
isDocked() -> Future<DockSide?>
Returns the current dock side, or null if not docked.
setAsFrameless() -> Future<void>
Removes the native window frame (title bar, border, etc.).
setAspectRatio(double aspectRatio) -> Future<void>
Locks the window to a fixed aspect ratio.
setBrightness(Brightness brightness) -> Future<void>
Sets the brightness (light/dark) of the window.
setIgnoreMouseEvents(bool ignore, {bool forward = false}) -> Future<void>
Makes the window ignore all mouse events.
popUpWindowMenu() -> Future<void>
Pops up the native window menu.
startDragging() -> Future<void>
Starts a window drag from a custom title bar widget.
startResizing(ResizeEdge resizeEdge) -> Future<void>
Starts a window resize from a custom border widget.
waitUntilReadyToShow(WindowOptions? options, VoidCallback? callback) -> Future<void>
Applies WindowOptions and calls callback once the window is ready to display.
invokeMethodToWindow(int targetWindowId, String method, dynamic args) -> Future
Sends an event to another window. The result is the return value of WindowListener.onEventFromWindow in the target window.
getDevicePixelRatio() -> double
Returns the device pixel ratio for this window.
toString() -> String
Returns a string representation including the window ID.
Static methods
ensureInitialized(int windowId) -> Future<void>
Initialize for the main window (windowId = 0). Must be called before accessing MultiWindowManager.current.
ensureInitializedSecondary(int windowId, {bool isEnabledReuse = false}) -> Future<void>
Initialize for a secondary window. Set isEnabledReuse: true to enable the reuse cache for this window.
createWindow(List\<String\>? args) -> Future<MultiWindowManager?>
Spawns a new window. Returns a MultiWindowManager instance for the new window, or null on failure.
createWindowOrReuse({List<String>? args}) -> Future<MultiWindowManager?>
Atomically claims a hidden reuse-cached window and reinitializes it with args, or spawns a new one if none are available. Safe to call from any window (main or secondary).
fromWindowId(int windowId) -> MultiWindowManager
Returns a MultiWindowManager instance for any registered window by ID.
getAllWindowManagerIds() -> Future<List<int>>
Returns the IDs of all windows currently registered in the process.
addGlobalListener(WindowListener listener) -> void
Subscribes to events from all windows. The windowId parameter in callbacks identifies the source window.
removeGlobalListener(WindowListener listener) -> void
Unsubscribes a global listener.
registry -> WindowRegistry
Process-wide reactive window registry. See WindowRegistry.
WindowListener
Mixin used with addListener / addGlobalListener. When used as a global listener, windowId identifies the source window; when used as a local listener it is always null.
onWindowClose(int? windowId) -> void
Emitted when the window is going to be closed.
onWindowFocus(int? windowId) -> void
Emitted when the window gains focus.
onWindowBlur(int? windowId) -> void
Emitted when the window loses focus.
onWindowMaximize(int? windowId) -> void
Emitted when the window is maximized.
onWindowUnmaximize(int? windowId) -> void
Emitted when the window exits a maximized state.
onWindowMinimize(int? windowId) -> void
Emitted when the window is minimized.
onWindowRestore(int? windowId) -> void
Emitted when the window is restored from a minimized state.
onWindowResize(int? windowId) -> void
Emitted while the window is being resized.
onWindowResized(int? windowId) -> void
Emitted once when the window finishes resizing. Windows / macOS only.
onWindowMove(int? windowId) -> void
Emitted while the window is being moved.
onWindowMoved(int? windowId) -> void
Emitted once when the window finishes moving. Windows / macOS only.
onWindowEnterFullScreen(int? windowId) -> void
Emitted when the window enters full-screen mode.
onWindowLeaveFullScreen(int? windowId) -> void
Emitted when the window leaves full-screen mode.
onWindowDocked(int? windowId) -> void
Emitted when the window enters a docked state. Windows only.
onWindowUndocked(int? windowId) -> void
Emitted when the window leaves a docked state. Windows only.
onWindowEvent(String eventName, int? windowId) -> void
Emitted for every window event, including reuse lifecycle events.
onEventFromWindow(String eventName, int fromWindowId, dynamic arguments) -> Future
Receives inter-window messages sent via invokeMethodToWindow. The return value is forwarded back to the caller.
WindowIpc
Accessible via MultiWindowManager.current.ipc. Lazy singleton - created on first access.
ipc -> WindowIpc<List<Object?>>
The IPC channel for this window. Payload type is always List<Object?>.
connectWindow(int targetId) -> Future<bool>
Establishes a bidirectional link with targetId. Returns false if the target has not yet accessed its ipc getter. After success, both windows can send and receive without further native calls.
notifyWindow(int targetId, List<Object?> data) -> Future<bool>
Sends data to targetId via SendPort. Calls connectWindow automatically on first use. Returns false if the target is not reachable.
listenWindow(int sourceId) -> Stream<List<Object?>>
Returns a broadcast stream of messages arriving from sourceId. Created lazily.
onConnected -> void Function(int fromWindowId)?
Callback fired when another window connects to this window (receives ipc-connect). Use it to set up stream subscriptions reactively.
onDisconnected -> void Function(int fromWindowId)?
Callback fired when a connected window sends a disconnect signal (on close or reuse-hide).
connectedWindowIds -> Set<int>
IDs of all windows this channel can currently send to.
IpcNotifierSender
Mixin for a ChangeNotifier that broadcasts field-level delta updates to other windows.
ipcSetup(WindowIpc<List<Object?>> ipc, String topic) -> void
Attaches the IPC channel and sets the routing topic. Multiple notifiers can share the same ipc with different topics.
ipcMark(String field, Object? value) -> void
Records that field changed to value and schedules a microtask flush. Call inside setters after updating the backing field and calling notifyListeners. Simultaneous changes within the same call stack are coalesced into one message.
IpcNotifierReceiver
Mixin for a ChangeNotifier that receives and applies delta updates from a source window.
ipcListen(WindowIpc<List<Object?>> ipc, int sourceId, String topic) -> void
Subscribes to updates tagged with topic from sourceId. May be called multiple times for different source windows.
ipcApplyField(String field, Object? value) -> void
Abstract. Override to apply a single incoming field change. notifyListeners is called once after all fields in the batch are applied.
ipcCancelListeners() -> void
Cancels all subscriptions. Call before dispose.
WindowRegistry
Accessible via MultiWindowManager.registry. Backed by shared native C++ state, so it is consistent across all Flutter engines in the process.
activeWindows -> ValueNotifier<List<int>>
IDs of currently visible / active windows. Updated automatically on every lifecycle event.
hiddenWindows -> ValueNotifier<List<int>>
IDs of reuse-cached hidden windows available for reclaiming. Updated automatically.
hasHiddenWindows -> bool
Whether at least one reusable window is available (reflects the last refresh result).
getActiveWindowIds() -> Future<List<int>>
Queries the native layer directly. Always returns a fresh value.
getHiddenWindowIds() -> Future<List<int>>
Queries the native layer directly. Always returns a fresh value.
refresh() -> Future<void>
Re-queries native state and updates activeWindows and hiddenWindows. Called automatically by MultiWindowManager on every lifecycle event.