multiview_desktop
Flutter desktop library for managing multiple OS windows from a single Flutter engine and a single Dart isolate.
Unlike libraries that spawn a new Flutter engine per window, multiview_desktop uses Flutter's multi-view API: all windows share one engine, one isolate, and one memory space. Opening a second window is as cheap as adding a new widget to the tree, and communication between windows is plain Dart with no isolate ports, no serialization, and no native bridge for passing data.
Platform Support
| Linux | macOS | Windows |
|---|---|---|
| + | + | + |
Linux note. The multi-view Flutter API on Linux currently requires a Wayland compositor. Running under an X11 session is not supported. Individual window control calls that depend on compositor positioning (such as
setPosition,setAlignment,center) may return silently when the compositor ignores client-side placement requests.
Architecture overview
runMultiApp starts a single Flutter engine with multi-view mode enabled. Every OS window is a separate FlutterView attached to that engine. The Dart code for all windows runs in the same isolate, so widgets and state objects can be passed around like any other Dart value.
This is the key difference from multi-engine approaches:
- Opening a window does not allocate a new VM, engine, or isolate.
- Widgets, streams,
ChangeNotifierinstances, and any Dart object can be shared directly across windows. No serialization or IPC channel is needed. WindowCommunicatoris provided as a lightweight routing helper, but sharing aValueNotifieror calling a method on a shared object is equally valid and often simpler.
Setup
Linux setup
Edit linux/runner/my_application.cc.
- Add the runner header alongside the other includes:
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
+#include <multiview_desktop/multiview_desktop_runner.h>
#include "flutter/generated_plugin_registrant.h"
- Add a
first-framecallback beforemy_application_activate. The primary window must stay hidden until Flutter paints its first frame; otherwise users see a blank window. Secondary windows opened by the runner follow the same pattern automatically.
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+// Called when first Flutter frame received.
+static void first_frame_cb(MyApplication* self, FlView* view) {
+ gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
+}
+
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
- In
my_application_activate, callmultiview_desktop_linux_runner_installbefore creating any window, callmultiview_desktop_linux_runner_prepare_dart_projectright afterfl_dart_project_new, and callmultiview_desktop_linux_runner_register_primaryafterfl_register_plugins. Connectfirst_frame_cbto the view'sfirst-framesignal and do not callgtk_widget_showon the window itself — the callback shows the top-level widget once rendering starts.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
+ multiview_desktop_linux_runner_install(GTK_APPLICATION(application));
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// ... (header bar setup, gtk_window_set_default_size - unchanged)
- gtk_window_set_default_size(window, 1280, 720);
+ gtk_window_set_default_size(window, 800, 600);
g_autoptr(FlDartProject) project = fl_dart_project_new();
+ multiview_desktop_linux_runner_prepare_dart_project(project);
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+ multiview_desktop_linux_runner_register_primary(window, view);
gtk_widget_grab_focus(GTK_WIDGET(view));
- gtk_widget_show(GTK_WIDGET(window));
}
What each call does:
first_frame_cb: shows the top-levelGtkWindowafter Flutter renders the first frame. Connect it withg_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self)and callgtk_widget_realizeon the view before registering plugins.multiview_desktop_linux_runner_install: hooks theGtkApplicationso that new GTK windows can be created when Dart callsopenWindow. Must be the very first call inactivate.multiview_desktop_linux_runner_prepare_dart_project: fixes asset, ICU, and AOT paths when launching from the build directory. Required so that secondary views can locate the bundle.multiview_desktop_linux_runner_register_primary: registers the primary window and view with the plugin so that per-window APIs work on the main window.
You can also set the default title for secondary windows before they appear:
multiview_desktop_linux_runner_set_default_title("My App");
Linux platform limitations
- Multi-view requires Wayland. The Flutter multi-view API on Linux currently works under Wayland compositors only. Running under a pure X11 session is not supported and the application will not open secondary windows.
- Window positioning on Wayland.
setPosition,setAlignment, andcenterusegtk_window_moveunder the hood. On Wayland the compositor controls window placement and the call is silently ignored. setAlwaysOnTop. Usesgtk_window_set_keep_above. Whether the compositor respects this hint depends on the desktop environment.setHasShadow. No-op on Linux. The native shadow is always drawn by the compositor.setMovable. Maps tosetResizableon Linux (there is no separate movability flag in GTK).setBadgeLabel,setVisibleOnAllWorkspaces,hideFromCollection. macOS-only. Not available on Linux.
Windows setup
The example windows/runner/flutter_window.cpp and flutter_window.h are replaced entirely. Copy the versions from the example app included in this package, or apply the following changes manually.
windows/runner/flutter_window.h: remove the FlutterViewController include and the flutter_controller_ field; keep only project_:
#include <flutter/dart_project.h>
-#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
class FlutterWindow : public Win32Window {
public:
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
flutter::DartProject project_;
- std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
flutter_controller_ is replaced entirely by the plugin. Leaving the field in the header will cause a compile error because flutter::FlutterViewController is no longer included.
windows/runner/flutter_window.cpp: replace the standard Flutter engine initialization with the multiview_desktop API:
#include "flutter_window.h"
#include <optional>
-#include "flutter/generated_plugin_registrant.h"
+#include <multiview_desktop/multi_view_desktop_plugin.h>
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
+ const int width = frame.right - frame.left;
+ const int height = frame.bottom - frame.top;
- flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
- frame.right - frame.left, frame.bottom - frame.top, project_);
- if (!flutter_controller_->engine() || !flutter_controller_->view()) {
- return false;
- }
- RegisterPlugins(flutter_controller_->engine());
- SetChildContent(flutter_controller_->view()->GetNativeWindow());
-
- flutter_controller_->engine()->SetNextFrameCallback([&]() {
- this->Show();
- });
-
- flutter_controller_->ForceRedraw();
+ MultiViewDesktopPrepareEngine(project_, GetHandle());
+ MultiViewDesktopCreateMainView(GetHandle(), width, height);
+ const HWND flutter_hwnd =
+ MultiViewDesktopGetFlutterHwnd(MultiViewDesktopGetMainViewId());
+ if (flutter_hwnd != nullptr) {
+ SetChildContent(flutter_hwnd);
+ }
+ CenterOnScreen();
return true;
}
void FlutterWindow::OnDestroy() {
- if (flutter_controller_) {
- flutter_controller_ = nullptr;
- }
-
Win32Window::OnDestroy();
}
LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
- if (flutter_controller_) {
- std::optional<LRESULT> result =
- flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
- lparam);
- if (result) {
- return *result;
- }
- }
-
- switch (message) {
- case WM_FONTCHANGE:
- flutter_controller_->engine()->ReloadSystemFonts();
- break;
- }
+ LRESULT result = 0;
+
+ if (message == WM_FONTCHANGE) {
+ FlutterDesktopEngineReloadSystemFonts(MultiViewDesktopGetEngineRef());
+ }
+ if (MultiViewDesktopHandleWindowProc(hwnd, message, wparam, lparam, &result)) {
+ return result;
+ }
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}
windows/runner/main.cpp: disable quit-on-close for the primary window:
FlutterWindow window(project);
- Win32Window::Point origin(10, 10);
- Win32Window::Size size(1280, 720);
+ Win32Window::Point origin(0, 0);
+ Win32Window::Size size(800, 600);
if (!window.Create(L"my_app", origin, size)) {
return EXIT_FAILURE;
}
-window.SetQuitOnClose(true);
+window.SetQuitOnClose(false);
Setting SetQuitOnClose(false) prevents the process from terminating when the main OS window is closed. The library takes over shutdown control via CloseMode.
Optional: CenterOnScreen helper
The example flutter_window.cpp calls CenterOnScreen() right after the main view is created. This positions the window at the center of the monitor immediately at the native level, before Dart has a chance to apply WindowOptions.alignment. Without it the window appears at the origin coordinates passed to Create and is only repositioned later when the Dart side runs. If you prefer to let Dart handle positioning exclusively you can skip this step and remove the CenterOnScreen() call from flutter_window.cpp.
If you want the native pre-center, add the method to the standard Flutter template files:
windows/runner/win32_window.h: add the declaration inside the public: section:
bool Create(const std::wstring& title, const Point& origin, const Size& size);
+ // Centers the window on the nearest monitor before the first frame.
+ void CenterOnScreen();
bool Show();
windows/runner/win32_window.cpp: add the implementation after the Create definition:
void Win32Window::CenterOnScreen() {
if (!window_handle_) {
return;
}
RECT rect{};
GetWindowRect(window_handle_, &rect);
const int width = rect.right - rect.left;
const int height = rect.bottom - rect.top;
const HMONITOR monitor =
MonitorFromWindow(window_handle_, MONITOR_DEFAULTTONEAREST);
MONITORINFO monitor_info{};
monitor_info.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(monitor, &monitor_info);
const int x = monitor_info.rcWork.left +
(monitor_info.rcWork.right - monitor_info.rcWork.left - width) / 2;
const int y = monitor_info.rcWork.top +
(monitor_info.rcWork.bottom - monitor_info.rcWork.top - height) / 2;
SetWindowPos(window_handle_, nullptr, x, y, width, height,
SWP_NOZORDER | SWP_NOACTIVATE);
}
SWP_NOZORDER | SWP_NOACTIVATE keeps the z-order unchanged and avoids stealing focus during initialization.
Windows platform limitations
setBadgeLabel,setVisibleOnAllWorkspaces,hideFromCollection. macOS-only. Not available on Windows.setProgressBar. Supported on Windows via taskbar progress API.
macOS setup
macos/Runner/MainFlutterWindow.swift: create the engine explicitly, call MultiviewDesktopPlugin.prepareEngine, and attach it to a FlutterViewController:
import Cocoa
import FlutterMacOS
+import multiview_desktop
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
+ let engine = FlutterEngine(
+ name: "main_flutter_engine",
+ project: nil,
+ allowHeadlessExecution: true
+ )
+ MultiviewDesktopPlugin.prepareEngine(engine, window: self)
+
+ let flutterViewController = FlutterViewController(engine: engine, nibName: nil, bundle: nil)
- let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: false)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}
MultiviewDesktopPlugin.prepareEngine enables multi-view mode on the engine, hides the window before the first frame, and stores a reference to the main NSWindow. This must be called before FlutterViewController is created.
macos/Runner/AppDelegate.swift: forward the two lifecycle callbacks to the plugin:
import Cocoa
import FlutterMacOS
+import multiview_desktop
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(
_ sender: NSApplication
) -> Bool {
- return true
+ return MultiviewDesktopPlugin.applicationShouldTerminateAfterLastWindowClosed()
}
+ override func applicationShouldHandleReopen(
+ _ sender: NSApplication,
+ hasVisibleWindows flag: Bool
+ ) -> Bool {
+ if MultiviewDesktopPlugin.applicationShouldHandleReopen(sender, hasVisibleWindows: flag) {
+ return true
+ }
+ return super.applicationShouldHandleReopen(sender, hasVisibleWindows: flag)
+ }
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
applicationShouldTerminateAfterLastWindowClosed is driven by CloseMode set from Dart, so the plugin controls whether the app stays alive after all windows close.
applicationShouldHandleReopen allows the library to restore the last window when the user clicks the dock icon after all windows have been hidden (relevant when using MacosPlatformParams.saveLastWindowToReopen).
Usage
Entry point
Replace runApp with runMultiApp. The home widget is rendered in the main OS window:
import 'package:flutter/material.dart';
import 'package:multiview_desktop/multiview_desktop.dart';
void main() {
runMultiApp(home: const MyApp());
}
runMultiApp calls WidgetsFlutterBinding.ensureInitialized internally, so you do not need to call it yourself.
globalScope
globalScope is an optional builder that wraps every OS window, including the main window and all secondary windows opened via openWindow. Use it to inject shared InheritedWidget providers, theme wrappers, or dependency-injection roots that every window needs access to:
void main() {
runMultiApp(
home: const MyApp(),
globalScope: (child) => MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthService()),
ChangeNotifierProvider(create: (_) => SettingsService()),
],
child: child,
),
);
}
The child argument passed to the builder is the window content. The builder is called once per window, so each window gets its own scope instance but can still share the same underlying Dart objects if those objects are allocated outside the builder.
Without globalScope, providers or inherited widgets placed in the home widget tree are not visible to secondary windows, because each window has its own separate widget subtree.
Optional: pass a MultiAppConfig to tune startup behavior:
void main() {
runMultiApp(
home: const MyApp(),
config: MultiAppConfig(
generalParams: MultiPlatformParams(
closeMode: CloseMode.cascade,
enableDynamicAnchor: true,
),
macosParams: MacosPlatformParams(
saveLastWindowToReopen: true,
closeAppAfterLastWindowClosed: false,
),
globalWindowOptions: WindowOptions(
size: const Size(1280, 720),
minimumSize: const Size(800, 600),
alignment: Alignment.center,
titleBarStyle: TitleBarStyle.normal,
title: 'My App',
),
),
);
}
globalWindowOptions are merged into every new window. Per-window options passed to openWindow take priority.
Open a window
Call openWindow from anywhere; you do not need a BuildContext:
// Open a window showing SettingsPage.
await openWindow(const SettingsPage());
// Open a window with custom options.
await openWindow(
const DashboardPage(),
options: WindowOptions(
size: const Size(1024, 768),
title: 'Dashboard',
titleBarStyle: TitleBarStyle.hidden,
alwaysOnTop: false,
),
);
openWindow returns the integer view ID of the new window.
If you need the new window to know which window opened it, pass parentContext:
await openWindow(
const DetailsPage(),
parentContext: context,
);
Inside DetailsPage, retrieve the parent context:
final parentScope = ParentWindowScope.of(context);
final parentContext = parentScope.parentContext;
if (parentContext != null && parentContext.mounted) {
final parentId = MultiViewDesktop.getIdByContext(parentContext);
}
Window options
WindowOptions describes the initial state applied when a window is created. All fields are optional; omitted fields fall back to globalWindowOptions from MultiAppConfig or built-in defaults.
| Field | Type | Description |
|---|---|---|
size |
Size? |
Initial content size in logical pixels. Default: 800x600. |
minimumSize |
Size? |
Minimum size the user can resize to. |
maximumSize |
Size? |
Maximum size the user can resize to. |
alignment |
Alignment? |
Where to place the window on the display (default: Alignment.center). |
backgroundColor |
Color? |
Native background color behind Flutter content. |
titleBarStyle |
TitleBarStyle? |
normal or hidden. |
windowButtonVisibility |
bool? |
Show or hide traffic-light / caption buttons when the bar is hidden. |
title |
String? |
Native window title. |
fullScreen |
bool? |
Start in full-screen mode. |
alwaysOnTop |
bool? |
Float above other windows. |
hideAppFromTaskbar |
bool? |
Hide the entire application from the dock / taskbar. |
Window events
Mix WindowListener into a State to receive lifecycle events for the window that owns the widget. Registration and cleanup are automatic; no addListener or removeListener calls are needed:
class _MyPageState extends State<MyPage> with WindowListener {
@override
void onWindowFocus() {
// The window gained keyboard focus.
setState(() {});
}
@override
void onWindowClose() {
// The user pressed the close button or closeWindow was called.
// If setPreventClose is true this fires instead of actually closing.
}
@override
void onWindowMaximize() {}
@override
void onWindowUnmaximize() {}
@override
void onWindowMinimize() {}
@override
void onWindowRestore() {}
@override
void onWindowResize() {}
@override
void onWindowResized() {} // macOS / Windows only, fires once when resize ends
@override
void onWindowMove() {}
@override
void onWindowMoved() {} // macOS / Windows only, fires once when move ends
@override
void onWindowEnterFullScreen() {}
@override
void onWindowLeaveFullScreen() {}
@override
void onWindowEvent(String eventName) {
// Every event by name; useful for logging or catching unlisted events.
}
}
The mixin registers for the window resolved from context during didChangeDependencies and unregisters in dispose. The currentId getter provides the view ID if you need it:
print('This window id: $currentId');
To subscribe to events for a specific window by ID (without using the mixin):
MultiViewDesktop.addListenerForView(viewId, myCallbacks);
MultiViewDesktop.removeListenerForView(viewId, myCallbacks);
Communication between windows
Because all windows share a single Dart isolate, you can pass any Dart object directly. The built-in WindowCommunicator provides a simple routing layer when you need to decouple senders from receivers.
Access it from anywhere:
final comm = MultiViewDesktop.communicator;
Direct messages
Send a message to a specific window by its view ID:
// Send from any window to window with id 2.
MultiViewDesktop.communicator.send(2, {'action': 'reload', 'tab': 'settings'});
Listen inside window 2:
// Subscribes to messages addressed to the window that owns context.
final sub = MultiViewDesktop.communicator.onDirect(context).listen((msg) {
if (msg is Map && msg['action'] == 'reload') {
setState(() { /* ... */ });
}
});
// Cancel in dispose:
sub.cancel();
You can also listen for messages addressed to a different window by passing viewId:
// In window 1, listen for messages sent to window 3.
MultiViewDesktop.communicator.onDirect(context, viewId: 3).listen((msg) { /* ... */ });
Broadcast messages
Send a message to every subscribed view at once:
// In any window: announce a theme change to all views.
MultiViewDesktop.communicator.broadcast({'type': 'themeMode', 'value': 'dark'});
Subscribe in any view:
final sub = MultiViewDesktop.communicator.onBroadcast.listen((msg) {
if (msg is Map && msg['type'] == 'themeMode') {
applyTheme(msg['value']);
}
});
sub.cancel(); // in dispose
Sharing state directly
For tightly coupled windows, sharing a ChangeNotifier or ValueNotifier directly is simpler than using the communicator:
// Defined once at the top level, accessible from every window.
final sharedTheme = ValueNotifier<ThemeMode>(ThemeMode.light);
// In any window:
sharedTheme.value = ThemeMode.dark;
// In any other window:
ValueListenableBuilder<ThemeMode>(
valueListenable: sharedTheme,
builder: (context, mode, _) => Text('Theme: $mode'),
);
Confirm before closing
Enable close interception on the window and respond in onWindowClose:
class _MyPageState extends State<MyPage> with WindowListener {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
MultiViewDesktop.ofContext(context).setPreventClose(true);
});
}
@override
void onWindowClose() async {
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) {
final win = MultiViewDesktop.ofContext(context);
await win.setPreventClose(false);
await win.closeWindow();
}
}
}
Close mode
CloseMode controls what happens to other open windows when the main window is closed.
Set it in MultiAppConfig.generalParams.closeMode at startup, or change it at runtime:
await MultiViewDesktop.setCloseMode(CloseMode.cascade);
| Mode | Behavior |
|---|---|
CloseMode.cascade |
Soft-close secondary windows one by one from newest to oldest, then soft-close the main window. Each window runs the full close cycle; use cancelCascadeClose inside onWindowClose to let the user abort. |
CloseMode.none |
Close only the main window. Secondary windows stay open. |
CloseMode.forceSecondary |
Force-close all secondary windows immediately, then soft-close the main window. |
CloseMode.destroy |
Force-close every window without running any close cycle. |
CloseMode.cascade is the default. It is the safest mode for apps that show unsaved-data dialogs, because each window gets a chance to respond before it is closed.
To abort a cascade close from inside a secondary window (for example after a user presses Cancel in a dialog):
@override
void onWindowClose() async {
final confirmed = await showUnsavedChangesDialog();
if (!confirmed) {
await MultiViewDesktop.ofContext(context).cancelCascadeClose();
}
}
Frameless windows
Pass TitleBarStyle.hidden to remove the native title bar. Then use WindowCaption or DragToMoveArea to let the user still drag the window.
Using WindowCaption
WindowCaption renders a 32 dp tall drag bar with an optional title widget. On Windows and Linux it also draws the minimize, maximize, and close buttons. On macOS the traffic-light buttons remain in their standard position.
@override
Widget build(BuildContext context) {
return Column(
children: [
const WindowCaption(
title: Text('My App'),
backgroundColor: Color(0xFF1E1E1E),
brightness: Brightness.dark,
),
const Expanded(child: MyContent()),
],
);
}
Set up the style when opening the window:
await openWindow(
const MyPage(),
options: WindowOptions(
titleBarStyle: TitleBarStyle.hidden,
backgroundColor: Colors.transparent,
),
);
Using DragToMoveArea
For a fully custom layout, wrap any region with DragToMoveArea to make it draggable:
DragToMoveArea(
child: Container(
height: 48,
color: Colors.blue,
child: const Center(child: Text('Drag here to move')),
),
)
Resizable edges
Add DragToResizeArea widgets at each edge or corner to restore user resizing when the native frame has been removed:
Stack(
children: [
// Main content
MyContent(),
// Bottom-right resize handle
Positioned(
right: 0,
bottom: 0,
child: DragToResizeArea(
resizeEdge: ResizeEdge.bottomRight,
child: const SizedBox(width: 12, height: 12),
),
),
],
)
All eight edges and corners are available: top, bottom, left, right, topLeft, topRight, bottomLeft, bottomRight.
Watching the window list
MultiViewDesktop.allViewsIds returns a snapshot list of all secondary view IDs currently open. allViewsIdsNotifier is a ValueNotifier updated every time a window opens or closes:
ValueListenableBuilder<List<int>>(
valueListenable: MultiViewDesktop.allViewsIdsNotifier,
builder: (context, ids, _) {
return Text('Open secondary windows: ${ids.length}');
},
)
Application config
MultiAppConfig is passed to runMultiApp once:
runMultiApp(
home: const MyApp(),
config: MultiAppConfig(
generalParams: MultiPlatformParams(
closeMode: CloseMode.cascade,
enableDynamicAnchor: true,
),
macosParams: MacosPlatformParams(
saveLastWindowToReopen: true,
closeAppAfterLastWindowClosed: false,
),
globalWindowOptions: WindowOptions(
size: const Size(1280, 720),
title: 'My App',
),
),
);
enableDynamicAnchor: when true, the library automatically tracks which window becomes the "anchor" (the last window visible). The anchor ID is accessible via MultiViewDesktop.getAnchorId().
saveLastWindowToReopen (macOS): when the user closes all windows and the app stays in the dock, re-opening from the dock icon restores the last window.
closeAppAfterLastWindowClosed (macOS): when false (the default), the process stays alive after the last window closes. The app icon remains in the dock. When true, the process terminates as soon as the last window closes.
API
MultiViewDesktop
Per-window methods are accessed through an instance obtained from a factory constructor:
final win = MultiViewDesktop.ofContext(context);
await win.setTitle('My Window');
await win.closeWindow();
// Or by view ID:
await MultiViewDesktop.fromId(viewId).setAlwaysOnTop(true);
App-wide operations (not targeting a specific window) are static:
await MultiViewDesktop.closeApp();
MultiViewDesktop.addListenerForView(viewId, listener);
Identity (static)
getIdByContext(BuildContext context) -> int
Returns the shifted view ID of the window that owns context.
allViewsIds -> List<int>
Snapshot of all secondary view IDs currently registered.
allViewsIdsNotifier -> ValueNotifier<List<int>>
Live-updating notifier. Fires whenever a window opens or closes. Use with ValueListenableBuilder.
Identity (instance)
id -> int
The shifted (public) view ID for this instance.
App-wide lifecycle (static)
openWindow(Widget child, {WindowOptions? options, BuildContext? parentContext}) -> Future<int>
Opens a new OS window showing child. Returns the view ID. Available as a top-level function; can be called without BuildContext.
closeApp({CloseMode? closeMode}) -> Future<void>
Closes all windows using closeMode (or the mode configured in MultiAppConfig).
setCloseMode(CloseMode closeMode) -> Future<void>
Changes the strategy used when the main window close button is pressed.
getCloseMode() -> CloseMode
Returns the currently active close mode.
setAnchorId(int viewId) -> Future<bool>
Sets the anchor view ID manually. Only valid for root views (views without a parent).
getAnchorId() -> int?
Returns the current anchor view ID, or null if none is set.
Per-window lifecycle (instance)
closeWindow() -> Future<void>
Soft-closes this window. If setPreventClose is true, emits onWindowClose instead of destroying the window.
isPreventClose() -> Future<bool>
Returns whether close is currently blocked for this window.
setPreventClose(bool isPreventClose) -> Future<void>
When true, any close attempt (native button or closeWindow) is blocked and onWindowClose fires instead. Set back to false to re-enable.
cancelCascadeClose() -> Future<void>
Aborts an in-progress CloseMode.cascade sequence that is waiting on this window.
Title and appearance (instance)
getTitle() -> Future<String>
Returns the native window title.
setTitle(String title) -> Future<void>
Changes the native window title shown in the title bar and dock tooltip.
setTitleBarStyle(TitleBarStyle style, {bool windowButtonVisibility = true}) -> Future<void>
Changes the title-bar style. Pass TitleBarStyle.hidden for a frameless window. windowButtonVisibility controls whether the traffic-light / caption buttons are still drawn when the bar is hidden.
getTitleBarStyle() -> Future<({TitleBarStyle? style, bool? buttonVisibility})>
Returns the current title-bar style and button visibility.
setAsFrameless() -> Future<void>
Removes the native title bar and border entirely.
setBackgroundColor(Color color) -> Future<void>
Sets the native window background color behind the Flutter view. Use Colors.transparent for a transparent window.
setBrightness(Brightness brightness) -> Future<void>
Sets the preferred appearance of native chrome (light or dark).
setOpacity(double opacity) -> Future<void>
Sets window opacity in the range 0.0 (fully transparent) to 1.0 (fully opaque).
getOpacity() -> Future<double>
Returns the current window opacity.
hasShadow() -> Future<bool>
Returns whether the window draws a native drop shadow.
setHasShadow(bool value) -> Future<void>
Enables or disables the native drop shadow. No-op on Linux.
Size and position (instance)
getBounds() -> Future<Rect>
Returns the window frame in Flutter logical coordinates (position and size combined).
getSize() -> Future<Size>
Returns the content size in logical pixels.
getPosition() -> Future<Offset>
Returns the top-left position of the window.
setSize(Size size) -> Future<void>
Resizes the window to size in logical pixels.
setPosition(Offset position) -> Future<void>
Moves the window so its top-left corner is at position. Silent on Wayland (Linux).
center() -> Future<void>
Centers the window on the screen that contains the largest portion of it.
setAlignment(Alignment alignment) -> Future<void>
Positions the window using alignment on the display under the cursor. Silent on Wayland (Linux).
setMinimumSize(Size size) -> Future<void>
Sets the minimum size the user can resize the window to.
setMaximumSize(Size size) -> Future<void>
Sets the maximum size the user can resize the window to.
setAspectRatio(double ratio) -> Future<void>
Locks the content area to a fixed aspect ratio (width / height). Pass 0 to remove the constraint.
Visibility and focus (instance)
show() -> Future<void>
Shows the window if it was hidden.
hide() -> Future<void>
Hides the window without closing it.
isVisible() -> Future<bool>
Returns whether the window is currently visible.
focus() -> Future<void>
Brings the window to the front and gives it keyboard focus.
blur() -> Future<void>
Removes keyboard focus from the window.
isFocused() -> Future<bool>
Returns whether this window is the current focused window.
Maximize, minimize, full screen (instance)
isMaximized() -> Future<bool>
Returns whether the window is in the maximized state.
maximize({bool vertically = false}) -> Future<void>
Maximizes the window.
unmaximize() -> Future<void>
Restores the window from the maximized state.
isMinimized() -> Future<bool>
Returns whether the window is minimized to the dock or taskbar.
minimize() -> Future<void>
Minimizes the window.
restore() -> Future<void>
Restores the window from the minimized state.
isFullScreen() -> Future<bool>
Returns whether the window is in native full-screen mode.
setFullScreen(bool isFullScreen) -> Future<void>
Enters or exits native full-screen mode.
Resizability and movability (instance)
isResizable() -> Future<bool>
Returns whether the user can resize the window by dragging its edges.
setResizable(bool isResizable) -> Future<void>
Enables or disables user resizing.
isMovable() -> Future<bool>
Returns whether the window can be moved by dragging the title bar.
setMovable(bool isMovable) -> Future<void>
Enables or disables moving the window by dragging. On Linux this maps to setResizable.
isMinimizable() -> Future<bool>
Returns whether the minimize button is enabled.
setMinimizable(bool isMinimizable) -> Future<void>
Enables or disables the minimize button and action.
isMaximizable() -> Future<bool>
Returns whether the maximize / zoom button is enabled.
setMaximizable(bool isMaximizable) -> Future<void>
Enables or disables the maximize button and action.
isClosable() -> Future<bool>
Returns whether the close button is enabled.
setClosable(bool isClosable) -> Future<void>
Enables or disables the close button and native close action.
Always on top and taskbar
isAlwaysOnTop() -> Future<bool> (instance)
Returns whether the window floats above normal application windows.
setAlwaysOnTop(bool isAlwaysOnTop) -> Future<void> (instance)
Keeps the window above other windows. On Linux depends on compositor support.
isHideAppFromTaskbar() -> Future<bool> (static)
Returns whether the application icon is hidden from the dock / taskbar (app-wide).
hideAppFromTaskbar(bool isHideAppFromTaskbar) -> Future<void> (static)
Hides or shows the application icon in the dock / taskbar app-wide.
isHideAppTabFromTaskbar() -> Future<bool> (instance)
Returns whether this specific window is hidden from the taskbar (Windows / Linux).
hideCurrentAppTabFromTaskbar(bool isHide) -> Future<void> (instance)
Hides or shows this window in the taskbar (Windows / Linux).
Drag and resize (instance, used by widgets)
startDragging() -> Future<void>
Starts a native window-move drag session. Called automatically by DragToMoveArea.
startResizing(ResizeEdge edge) -> Future<void>
Starts a native window-resize drag session from edge. Called automatically by DragToResizeArea.
Mouse events (instance)
setIgnoreMouseEvents(bool ignore, {bool mouseMoveEvents = false}) -> Future<void>
When ignore is true, all mouse events pass through the window. If mouseMoveEvents is true, mouse move events still arrive despite ignore being set.
isIgnoreMouseEvents() -> Future<({bool mouseMoveEvents, bool ignore})>
Returns the current mouse pass-through state.
popUpWindowMenu() -> Future<void>
Shows the native window context menu at the current cursor position (macOS).
macOS-specific (instance)
isHideFromCollection() -> Future<bool>
Returns whether the window is excluded from Mission Control (macOS).
hideFromCollection(bool isHideFromCollection) -> Future<void>
Hides or shows the window in Mission Control and Expose (macOS).
isVisibleOnAllWorkspaces() -> Future<bool>
Returns whether the window is pinned to all Spaces (macOS).
setVisibleOnAllWorkspaces(bool visible, {bool visibleOnFullScreen = false}) -> Future<void>
Pins or unpins the window across all Spaces (macOS).
setBadgeLabel({String? label}) -> Future<void>
Sets the dock icon badge text for this window (macOS). Pass null to clear the badge.
Progress bar
setProgressBar(double progress) -> Future<void>
Sets the taskbar / dock progress indicator from 0.0 to 1.0. App-wide on Windows. macOS shows progress in the dock.
WindowListener
Mixin for State. Automatically registers for events of the window that owns the widget, and unregisters on dispose. Override only the callbacks you need; all have empty default implementations.
onWindowClose() -> void
Fires when the window is going to close (or when close is blocked by setPreventClose).
onWindowFocus() -> void
Fires when the window gains keyboard focus.
onWindowBlur() -> void
Fires when the window loses focus.
onWindowMaximize() -> void
Fires when the window is maximized.
onWindowUnmaximize() -> void
Fires when the window exits the maximized state.
onWindowMinimize() -> void
Fires when the window is minimized.
onWindowRestore() -> void
Fires when the window is restored from a minimized state.
onWindowResize() -> void
Fires continuously while the user drags the window edge.
onWindowResized() -> void
Fires once when the user finishes resizing. macOS and Windows only.
onWindowMove() -> void
Fires continuously while the user drags the window.
onWindowMoved() -> void
Fires once when the user finishes moving the window. macOS and Windows only.
onWindowEnterFullScreen() -> void
Fires when the window enters full-screen mode.
onWindowLeaveFullScreen() -> void
Fires when the window exits full-screen mode.
onWindowEvent(String eventName) -> void
Fires for every window event by name. Useful for logging or handling events not covered by the named callbacks.
currentId -> int?
The view ID that this listener is currently registered for.
WindowCommunicator
In-process message bus. Accessible via MultiViewDesktop.communicator.
Because all windows run in the same Dart isolate, messages are never serialized. WindowCommunicator is a thin routing layer that decouples senders from receivers. For simple shared state a shared ValueNotifier or ChangeNotifier is often more direct.
send(int viewId, dynamic message) -> void
Delivers message to every active listener registered for viewId via onDirect. If no one is listening the message is dropped silently.
broadcast(dynamic message) -> void
Delivers message to every active onBroadcast subscriber in every view simultaneously.
onDirect(BuildContext context, {int? viewId}) -> Stream<dynamic>
Returns a broadcast Stream of messages sent to viewId via send. When viewId is omitted, listens on the window that owns context.
onBroadcast -> Stream<dynamic>
A broadcast Stream that receives every message sent via broadcast. Subscribe in any view.
WindowOptions
Initial configuration for a window. Passed to openWindow or set as globalWindowOptions in MultiAppConfig.
| Field | Type | Default | Description |
|---|---|---|---|
size |
Size? |
800x600 | Content size in logical pixels. |
minimumSize |
Size? |
none | Minimum resizable size. |
maximumSize |
Size? |
none | Maximum resizable size. |
alignment |
Alignment? |
Alignment.center |
Placement on the display. |
backgroundColor |
Color? |
platform | Native background color. |
titleBarStyle |
TitleBarStyle? |
normal |
normal or hidden. |
windowButtonVisibility |
bool? |
true |
Show caption buttons when bar is hidden. |
title |
String? |
none | Native window title. |
fullScreen |
bool? |
false |
Start in full-screen mode. |
alwaysOnTop |
bool? |
false |
Float above other windows. |
hideAppFromTaskbar |
bool? |
false |
Hide app from dock / taskbar. |
MultiAppConfig
Passed to runMultiApp once.
generalParams -> MultiPlatformParams
Cross-platform parameters.
closeMode - the CloseMode used when the main window closes. Default: CloseMode.cascade.
enableDynamicAnchor - when true, automatically tracks the last visible window as the anchor. Default: true.
macosParams -> MacosPlatformParams
macOS-specific parameters.
saveLastWindowToReopen - restore the last window when the dock icon is clicked after all windows close. Default: true.
closeAppAfterLastWindowClosed - quit the process when the last window closes. Default: false.
globalWindowOptions -> WindowOptions
Default WindowOptions merged into every new window. Per-window options override these.
CloseMode
Controls what happens to other windows when the main window close button is pressed.
cascade
Default. Soft-closes secondary windows one by one from newest to oldest, then soft-closes the main window. Each window runs through the full close cycle (prevent-close check, onWindowClose). Use cancelCascadeClose inside a confirmation dialog to let the user abort without losing unsaved work.
none
Closes only the main window. Secondary windows stay open.
forceSecondary
Force-closes all secondary windows immediately, then soft-closes the main window.
destroy
Force-closes every window without running any close cycle.
Widgets
WindowCaption
A ready-made 32 dp tall custom title bar for frameless windows. Renders a DragToMoveArea and, on Windows and Linux, minimize / maximize / close buttons. On macOS the traffic-light buttons stay in their standard position; WindowCaption adds left padding so the title does not overlap them.
const WindowCaption(
title: Text('My App'),
backgroundColor: Color(0xFF2C2C2C),
brightness: Brightness.dark,
)
| Field | Type | Description |
|---|---|---|
title |
Widget? |
Widget shown in the title bar. |
backgroundColor |
Color? |
Fill color for the bar area. |
brightness |
Brightness? |
Foreground color for text and icons (light = dark icons, dark = white icons). Default: Brightness.light. |
DragToMoveArea
Wraps any widget and starts a native window-move session when the user drags on it.
DragToMoveArea(
child: Container(height: 48, color: Colors.blueGrey),
)
Double-tap on the area is absorbed so it does not accidentally trigger maximize.
DragToResizeArea
Starts a native resize session from a specific edge or corner when the user drags on it. Place one instance per edge or corner you want to be resizable.
DragToResizeArea(
resizeEdge: ResizeEdge.bottomRight,
enableResizeEdge: true, // optional: disable dynamically
child: const SizedBox(width: 8, height: 8),
)
ResizeEdge |
Description |
|---|---|
top |
Top edge |
bottom |
Bottom edge |
left |
Left edge |
right |
Right edge |
topLeft |
Top-left corner |
topRight |
Top-right corner |
bottomLeft |
Bottom-left corner |
bottomRight |
Bottom-right corner |
License
Libraries
- multiview_desktop
- Single-engine multi-window library for Flutter desktop.