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
- Architecture overview
- Setup
- Linux
- Windows
- macOS
- Usage
- Entry point
- Entry shell (AppShell)
- Open a window
- Open a dialog
- Open a popup
- Deferred show (
showOnInit) - Animations
- Window options
- Dialog options
- Window events
- Communication between windows
- Confirm before closing
- Close mode
- Frameless windows
- Watching the window list
- Window observers
- Application config
- Logging
- API
- MultiViewDesktop
- AppShell
- WindowListener
- WindowObserver
- WindowCommunicator
- WindowOptions
- DialogOptions
- MultiAppConfig
- CloseMode
- ViewAnimationConfig
- PopupView
- Widgets
Platform Support
| Linux | macOS | Windows |
|---|---|---|
| yes | yes | yes |
Linux note. Multi-view on Linux works under both X11 and Wayland. On Wayland, the compositor controls window placement, so
setPosition,setAlignment, andcentermay be ignored silently. On X11, client-side positioning is supported.PopupViewneeds that positioning, so popups are disabled on Linux without X11 (Wayland). UseGDK_BACKEND=x11if you need popups.
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. Native window chrome (size, position, title bar, and similar) is driven through a small FFI layer; application code talks to MultiViewDesktop and never to that layer directly.
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.
Native bridge (FFI, 2.0)
From 2.0, native window calls use FFI instead of MethodChannel.
Most chrome / state APIs are synchronous - do not await them:
final win = MultiViewDesktop.of(context);
win.setTitle('Settings');
win.setAlwaysOnTop(true);
win.focus();
final focused = win.isFocused();
Keep await only where the operation is inherently asynchronous (create/close animation, geometry animation, or a Future-returning API):
await openWindow((_, __) => const SettingsPage());
await win.setSize(const Size(900, 640));
await win.setPosition(const Offset(100, 80));
await win.closeWindow();
Typical sync surface: title, title-bar style, opacity, brightness, shadow, min/max size getters/setters (except aspect-ratio resize), visibility (show / hide), focus, maximize / minimize / full-screen flags, always-on-top, mouse pass-through, prevent-close, and similar.
Typical async surface: openWindow / openDialog / closeWindow / closeDialog / closeApp, setSize / setPosition / setAlignment / center / setDialogAlignment / setAspectRatio, and popup open / close / toggle.
You do not call the FFI layer yourself. Platform setup (runner / MainFlutterWindow / AppDelegate) is unchanged in shape from 1.x; only the Dart call style above matters for migrating app code.
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"
- 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. Do not callgtk_widget_showon theGtkWindow, and do not connect afirst-framecallback that shows it. The primary window stays unmapped until the library shows it (showOnInit/completeShow).
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));
- // Show the window when Flutter renders.
- 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));
}
- In
my_application_new, create theGtkApplicationwith the defaultGApplicationflags. Do not passG_APPLICATION_NON_UNIQUE: the app must register its D-Bus name (application-id) so taskbar / dock context menu items work.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
- "application-id", APPLICATION_ID, "flags",
- G_APPLICATION_NON_UNIQUE, nullptr));
+ "application-id", APPLICATION_ID,
+ nullptr));
What each call does:
- The primary
GtkWindowstays unmapped. Callgtk_widget_realizeon the view, thenmultiview_desktop_linux_runner_register_primary. The library maps the window fromshowOnInit/completeShow. 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");
Taskbar / dock context menu (Linux)
Set items via MultiPlatformParams.menuItems in runMultiApp, or replace them at runtime with MultiViewDesktop.setMenuItems.
Requirements:
- Default
GApplicationflags inmy_application_new(step 3 above).G_APPLICATION_NON_UNIQUEprevents D-Bus registration and breaks the menu. g_set_prgname(APPLICATION_ID)so the running app matches the generated.desktopentry.
The plugin registers GApplication actions and writes ~/.local/share/applications/<application-id>.desktop with DBusActivatable=true. GNOME and other freedesktop shells that expose .desktop Actions in the dock context menu pick up the items after the app starts (restart the app once if the menu does not appear immediately).
Linux platform limitations
- X11 and Wayland. Multi-view works on both session types. Under X11 the runner installs error handling for Flutter's multi-threaded GL rendering and defers window destruction to avoid raster-thread races.
- Window positioning on Wayland.
setPosition,setAlignment, andcenterusegtk_window_moveunder the hood. On Wayland the compositor controls window placement and the call is silently ignored. On X11 these calls use client-side coordinates. PopupView. Disabled on Linux without X11. A popup is a native window that must be placed next to its trigger; that requires client-side positioning, which Wayland does not allow. On X11 (includingGDK_BACKEND=x11) popups work. On Wayland,PopupView/PopupController.opendo not create a popup.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).macos.setBadgeLabel,macos.setVisibleOnAllWorkspaces,macos.hideFromCollection,macos.isOnActiveSpace. macOS-only (MultiViewDesktop.of(context).macos). Not available on Linux.setProgressBar. Not available on Linux.TaskbarMenuItem.iconAsset. Not available on Linux; dock menu items show the title only.- Taskbar / dock context menu. Supported via
menuItems/setMenuItems; see Taskbar / dock context menu (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: keep flutter/generated_plugin_registrant.h. The library owns the engine, so pass RegisterPlugins into MultiViewDesktopCreateMainView (do not call RegisterPlugins(flutter_controller_->engine())). The callback runs after the primary view exists so plugins that need GetView() can register. Then parent the Flutter HWND with SetChildContent:
#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, RegisterPlugins);
+ 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);
}
MultiViewDesktopHandleWindowProc replaces flutter_controller_->HandleTopLevelWindowProc. It forwards HWND messages to plugins that use RegisterTopLevelWindowProcDelegate (tray, etc.) and runs MVD close handling. Do not call the engine API yourself: the engine treats WM_CLOSE as process exit.
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.
Taskbar jump list forwarding (required for custom taskbar menu callbacks while the app is already running):
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+MultiViewDesktopInitializeShellIntegration();
+
if (MultiViewDesktopTryForwardTaskbarMenuActivation()) {
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
+
flutter::DartProject project(L"data");
When the user selects a jump list item, Windows may start a second process with --mvd-taskbar-menu=<id>. The helper forwards that activation to the running instance and exits the duplicate process.
Taskbar / jump list context menu (Windows)
Set items via MultiPlatformParams.menuItems in runMultiApp, or replace them at runtime with MultiViewDesktop.setMenuItems.
Requirements:
- Call
MultiViewDesktopInitializeShellIntegration()inwWinMainafterCoInitializeExand before creating the window (see diff above). - Call
MultiViewDesktopTryForwardTaskbarMenuActivation()so jump list clicks reach the running process when the app is already open. - Optional
TaskbarMenuItem.iconAssetper item (PNG asset shown in the jump list).
Items appear in the taskbar jump list when the user right-clicks the app icon. Windows uses an internal AppUserModelID derived from the executable file name (for example MultiviewDesktop.my_app). Users do not see this ID; it is unrelated to the window title. Keep the .exe name stable across releases so pinned taskbar entries keep the same jump list.
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
macos.setBadgeLabel,macos.setVisibleOnAllWorkspaces,macos.hideFromCollection,macos.isOnActiveSpace. macOS-only (MultiViewDesktop.of(context).macos). Not available on Windows.setProgressBar. Supported on Windows via taskbar progress API.- Taskbar / jump list context menu. Supported via
menuItems/setMenuItems; see Taskbar / jump list context menu (Windows). - Taskbar menu icons.
TaskbarMenuItem.iconAssetis supported on Windows (jump list) and macOS (dock menu). On Linux, menu items work but icons are ignored.
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 lifecycle and dock-menu 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
}
+
+ override func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
+ return MultiviewDesktopPlugin.applicationShouldTerminate(sender)
+ }
+
+ override func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
+ return MultiviewDesktopPlugin.applicationDockMenu(sender)
+ }
}
applicationShouldTerminateAfterLastWindowClosedis driven byCloseModeset from Dart, so the plugin controls whether the app stays alive after all windows close.applicationShouldHandleReopenrestores hidden windows when the user clicks the dock icon (relevant withMacosPlatformParams.saveLastWindowToReopen).applicationShouldTerminateintercepts Cmd+Q and Quit from the menu. The plugin callsMacosPlatformParams.onTerminate; returntruefrom the callback to quit,falseto cancel.applicationDockMenushowsMultiPlatformParams.menuItems/MultiViewDesktop.setMenuItemsin the dock context menu.
Usage
Entry point
Replace runApp with runMultiApp. The home builder is rendered in the main OS window:
import 'package:flutter/material.dart';
import 'package:multiview_desktop/multiview_desktop.dart';
void main() {
runMultiApp(
home: (context, id) => MaterialApp(
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
home: const HomePage(),
),
);
}
runMultiApp calls WidgetsFlutterBinding.ensureInitialized internally, so you do not need to call it yourself.
The main window should include a root entry widget (MaterialApp, CupertinoApp, or WidgetsApp). The library reads app-wide fields from it (theme, locale, shortcuts, and similar) and reuses them for secondary windows and dialogs. See Entry shell (AppShell).
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: (context, id) => 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: (context, id) => const MyApp(),
config: MultiAppConfig(
generalParams: MultiPlatformParams(
closeMode: CloseMode.softCascade,
enableDynamicAnchor: true,
closeAppAfterLastWindowClosed: true,
animation: ViewAnimationConfig.defaults,
menuItems: [
TaskbarMenuItem(
title: 'Open new window',
onPressed: () => openWindow((ctx, id) => const HomePage()),
),
],
),
macosParams: MacosPlatformParams(
saveLastWindowToReopen: true,
onTerminate: () async {
final allClosed = await MultiViewDesktop.closeApp(closeMode: CloseMode.softCascade);
return allClosed;
},
),
globalWindowOptions: WindowOptions(
size: const Size(1280, 720),
minimumSize: const Size(800, 600),
alignment: Alignment.center,
titleBarStyle: TitleBarStyle.normal,
title: 'My App',
),
globalDialogOptions: DialogOptions(modal: false),
fileLogParams: const LogParams(enable: false),
observers: [AppWindowObserver()],
),
);
}
globalWindowOptions are merged into every new window. Per-window options passed to openWindow take priority.
globalDialogOptions are merged into every openDialog call. See Open a dialog.
Entry shell (AppShell)
Each OS window is a separate Flutter View with its own widget subtree. Theme, Locale, and other InheritedWidget values from the main MaterialApp are not visible in secondary windows or dialogs.
The library solves this with a shared entry shell:
- Main window. Your
homebuilder returns a full entry widget (MaterialApp,CupertinoApp, orWidgetsApp) with navigation. - Capture. While the main window is open,
MainAppShellCapturescans that entry widget after every frame and copies app-wide fields into an internal registry (MultiViewDesktop.appShell). - Secondary and dialog views. Content from
openWindow/openDialogis wrapped inSharedEntryApp, which builds a matching entry shell around your page widget. Navigation stays per-view; appearance is inherited from the registry plus optional overrides.
What goes where
| Layer | Scope | Examples |
|---|---|---|
Global registry (MultiViewDesktop.appShell) |
All secondary windows and dialogs | theme, darkTheme, themeMode, locale, localizationsDelegates |
ViewShellOverrides on one view |
That window or dialog only | Different locale, different router, per-view theme override |
Main MaterialApp in home |
Main window only | Full navigation, your own state management |
Do not wrap secondary content in a second full MaterialApp. Pass page widgets to openWindow and customize the shell through WindowOptions.shellOverrides or MultiViewDesktop.patchViewShell.
Updating theme and locale across windows
MultiViewDesktop.appShell.patch and .apply rebuild secondary and dialog shells. They do not rebuild the main window automatically.
The main window does not listen to appShell on purpose: calling patch from the main build method would create a feedback loop (patch triggers registry update, registry triggers rebuild, rebuild calls patch again).
Keep the main window in sync manually:
void setThemeMode(ThemeMode mode) {
_themeMode = mode;
MultiViewDesktop.appShell.patch(AppShellPatch(themeMode: mode));
notifyListeners(); // rebuild main MaterialApp from the same notifier
}
You can read MultiViewDesktop.appShell.snapshot in a ListenableBuilder, but do not call patch from that builder.
Per-view customization (ViewShellOverrides)
At open time:
openWindow(
(_, __) => PreviewPage(),
options: WindowOptions(
shellOverrides: ViewShellOverrides(
appearance: AppShellPatch(
locale: const Locale('de'),
themeMode: ThemeMode.dark,
),
),
),
);
At runtime (this view only):
MultiViewDesktop.of(context).patchViewShell(
ViewShellOverrides.appearance(AppShellPatch(locale: const Locale('en'))),
);
Dedicated router on one secondary window:
openWindow(
(_, __) => const SizedBox.shrink(),
options: WindowOptions(
shellOverrides: ViewShellOverrides(routerConfig: settingsRouter),
),
);
Native window chrome brightness (title bar on macOS) follows the effective themeMode (global plus per-view override). The library calls setBrightness when a secondary view or dialog is created and when the shell changes.
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);
}
Open a dialog
openDialog is supported on Linux, macOS, and Windows. Unlike openWindow, it requires parentContext from a registered window. A dialog cannot open another dialog: the parent must be in the windows registry, not in the dialogs registry.
Wrap the main window content in DialogModalLayer when you use modal dialogs so the parent shows a Flutter scrim:
runMultiApp(
home: (context, id) => DialogModalLayer(
child: MaterialApp(home: HomePage()),
),
);
final result = await openDialog<String>(
(context, id) => const SettingsDialog(),
parentContext: context,
options: DialogOptions(title: 'Settings', modal: true),
);
// Inside the dialog:
await MultiViewDesktop.of(context).closeDialog('saved');
Dialogs close automatically when their parent window closes, regardless of CloseMode. Full-screen mode is not available. Minimize and maximize are disabled on the native title bar.
Modeless dialog (modal: false)
A reduced window tied to a parent:
- No full-screen, minimize, or maximize
- Does not block the parent at the OS level
- Can be positioned relative to the parent or on screen (platform-dependent)
Modal dialog (modal: true)
Same window restrictions as modeless, plus the parent is blocked natively while the dialog is open. Add DialogModalLayer on the parent for a visual dimming overlay; the scrim alone does not block OS input.
On macOS only, a modal dialog is shown as a sheet fixed to the parent window: it stays centered on the parent and cannot be moved outside it. On Windows and Linux, a modal dialog still blocks the parent, but the user can drag it anywhere on screen, including outside the parent bounds.
Open/close fade from ViewAnimationConfig applies to modal dialogs only on Windows. On macOS and Linux the native presentation already animates (sheet / WM), so the library fade is skipped. See Animations.
Platform differences
| Behavior | macOS | Windows | Linux |
|---|---|---|---|
| Modeless positioning | Centered over parent at open; can be moved freely after | Centered inside parent bounds at open; can move outside parent | Can move anywhere on screen |
| Modal positioning | Sheet on parent; fixed inside parent, not positioned from Dart | Centered inside parent at open; can move outside parent | Can move anywhere on screen |
| Modal fixed inside parent | yes | no | no |
| Modal blocks parent input | yes (sheet) | yes (owner window) | yes (transient + input lock) |
| Modal library fade | no (native sheet) | yes | no (native / WM) |
| Modeless blocks parent | no | no | no |
See Dialog options for DialogOptions fields and Window observers for dialog lifecycle callbacks.
Watch open dialogs:
ValueListenableBuilder<List<int>>(
valueListenable: MultiViewDesktop.allDialogIdsNotifier,
builder: (context, ids, _) {
return Text('Open dialogs: ${ids.length}');
},
)
Open a popup
PopupView opens a small native OS window anchored to a widget. Unlike a Flutter overlay, the popup is a real window: it can overlap other windows and leave the parent bounds. The child is built once per PopupController.open and reused if the trigger is unmounted (for example while scrolling a ListView).
Requires an Overlay ancestor (MaterialApp provides one).
Supported on macOS, Windows, and Linux with X11. On Linux without X11 (Wayland) popups are disabled: the compositor owns window placement, and a popup cannot be positioned next to its trigger. Use GDK_BACKEND=x11 if you need popups on Linux.
final controller = PopupController();
PopupView(
controller: controller,
positioner: const PopupPositioner(
parentAnchor: PopupPositionerAnchor.bottomLeft,
childAnchor: PopupPositionerAnchor.topLeft,
),
builder: (context) => const MyMenu(),
child: TextButton(
onPressed: controller.toggle,
child: const Text('Menu'),
),
);
open(), close(), and toggle() on the controller are the user-facing API. Pass animation: to override the popup open/close fade for that call.
While the popup is open, controller.viewController exposes chrome that does not affect placement: opacity, shadow, background color (Windows), and mouse pass-through.
If the trigger leaves the visible clip or PopupView is unmounted, the native window is hidden until a visible PopupView is attached to the same controller again. close() ends the session and disposes the child.
PopupPositioner
| Field | Default | Description |
|---|---|---|
parentAnchor |
bottomLeft |
Point on the trigger used as the attachment origin. |
childAnchor |
topLeft |
Point on the popup aligned to parentAnchor. |
offset |
Offset.zero |
Extra translation after aligning the anchors. |
constraintAdjustment |
flip Y, slide X | What to do when the popup would leave the display. |
PopupConstraintAdjustment flags: flipX, flipY, slideX, slideY, resizeX, resizeY.
See PopupView for the API summary.
Deferred show (showOnInit)
showOnInit (default true) is available on both WindowOptions and DialogOptions. When false, the view is created and attached to the Flutter tree, but the native surface stays hidden until you call completeShow() on that view.
Use this to prepare content (load data, wait for first layout) before the OS window appears.
await openWindow(
(context, viewId) {
final win = MultiViewDesktop.fromId(viewId);
// Prepare UI, then reveal:
WidgetsBinding.instance.addPostFrameCallback((_) async {
await loadSettings();
win.completeShow();
});
return const SettingsPage();
},
options: const WindowOptions(
showOnInit: false,
size: Size(900, 640),
),
);
await openDialog(
(context, viewId) {
final dialog = MultiViewDesktop.fromId(viewId);
Future<void>.delayed(const Duration(milliseconds: 500), dialog.completeShow);
return const ConfirmDialog();
},
parentContext: context,
options: const DialogOptions(showOnInit: false, modal: false),
);
Notes:
- The startup window follows the same flag (
mainWindowOptions, orglobalWindowOptionswhen main options are omitted). - Call
completeShow(), notshow().show()is the normal visibility API for an already-shown-or-hidden window;completeShowfinishes the create pipeline whenshowOnInitwasfalse. - Early
completeShowbefore the create path registers the waiter is buffered and applied when create reaches the wait (safe for post-frame / delayed callbacks). - For modal dialogs,
completeShowgates the native attach step (beginSheeton macOS / equivalent on other platforms), not a plainshow. openWindow/openDialogFutures still complete only after the create cycle finishes (includingcompleteShowwhen deferred).
Animations
Native window animations are configured once in MultiAppConfig.generalParams.animation. By default, windows, modeless dialogs, and popups fade in and out; size/position animation is off.
Modal dialogs: library open/close fade is Windows-only. macOS (sheet) and Linux (WM) use their own presentation animation.
runMultiApp(
home: (context, id) => const MyApp(),
config: MultiAppConfig(
generalParams: MultiPlatformParams(
animation: ViewAnimationConfig.all(),
),
),
);
| Factory | What it enables |
|---|---|
ViewAnimationConfig.defaults |
Open/close fade for windows, modeless dialogs, and popups. Geometry off. |
ViewAnimationConfig.disabled |
Everything off. |
ViewAnimationConfig.openClose(...) |
Open/close fade only. |
ViewAnimationConfig.geometry(...) |
setSize, setPosition, setAlignment, and center only. |
ViewAnimationConfig.all(...) |
Fade and geometry. |
Pass fps: to drive ticks on a timer. When omitted, ticks follow display frames.
Soft override
Pass animation: on a single call. Applied only when that animation type is already enabled in config:
await openWindow(
(_, __) => const SettingsPage(),
animation: const AnimationSettings(
duration: Duration(milliseconds: 280),
curve: Curves.easeOutCubic,
),
);
await MultiViewDesktop.of(context).setSize(
const Size(900, 640),
animation: const AnimationSettings(duration: Duration(milliseconds: 200)),
);
await controller.open(
animation: const AnimationSettings(duration: Duration(milliseconds: 120)),
);
Works on openWindow, openDialog, closeWindow, closeDialog, setSize, setPosition, setAlignment, center, and popup open / close / toggle.
Force override
setForceAnimation stages a one-shot animation that runs even if that type is disabled. It is consumed by the next matching operation. Not used for popups.
final win = MultiViewDesktop.of(context);
win.setForceAnimation(
ViewAnimationType.setSize,
const AnimationSettings(duration: Duration(milliseconds: 400)),
);
await win.setSize(const Size(800, 600));
See ViewAnimationConfig.
Window options
WindowOptions is passed to openWindow or set once as globalWindowOptions in MultiAppConfig. Fields you omit fall back to global defaults, then to built-in defaults.
Per-call options override globalWindowOptions. Dialogs use a separate type, DialogOptions, with its own global defaults (globalDialogOptions).
Shared appearance fields
These fields exist on both WindowOptions and DialogOptions with the same meaning:
| Field | Type | Description |
|---|---|---|
size |
Size? |
Initial content size in logical pixels. |
minimumSize |
Size? |
Minimum size the user can resize to. |
maximumSize |
Size? |
Maximum size the user can resize to. |
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. On dialogs, minimize and maximize stay disabled regardless of this flag. |
title |
String? |
Native window title. |
alwaysOnTop |
bool? |
Keep the view above other application windows. |
maximize |
bool? |
Start maximized. For windows, full-screen takes priority over maximize. |
showOnInit |
bool? |
Show immediately after creation (default true). When false, call completeShow() to reveal. Applies to the startup window as well. See Deferred show. |
shellOverrides |
ViewShellOverrides? |
Per-view entry shell (theme, locale, router). See Entry shell (AppShell). |
Built-in default content size for windows when size is omitted: 800x600.
Window-only fields
| Field | Type | Description |
|---|---|---|
alignment |
Alignment? |
Where to place the window on the display (default: Alignment.center). Not used for dialogs. |
fullScreen |
bool? |
Start in full-screen mode. Not available for dialogs. |
Example:
openWindow(
(_, __) => const SettingsPage(),
options: WindowOptions(
size: const Size(900, 640),
title: 'Settings',
alignment: Alignment.center,
shellOverrides: ViewShellOverrides(
appearance: AppShellPatch(locale: const Locale('de')),
),
),
);
Dialog options
DialogOptions is passed to openDialog or set once as globalDialogOptions in MultiAppConfig. Merge rules are the same as for windows: per-call options override global defaults.
Reuse shared appearance fields for size, title, titleBarStyle, backgroundColor, shellOverrides, and the rest. Built-in default content size for dialogs when size is omitted: 400x300.
Dialog-only fields
| Field | Type | Default | Description |
|---|---|---|---|
modal |
bool? |
false |
When true, blocks the parent at the OS level while the dialog is open. See Open a dialog for platform behavior. |
isResizable |
bool? |
platform | Whether the user can resize the dialog by dragging edges. |
showOnInit is a shared field (see above). For dialogs it has the same meaning as for windows; for modal dialogs it delays the native parent attach until completeShow().
Restrictions (not configurable)
Dialogs always differ from regular windows:
- Require
parentContextfrom a window (not from another dialog). - Close when the parent window closes, regardless of
CloseMode. - No full-screen or minimize. The title bar exposes close only.
maximizestill starts the dialog maximized. - Hidden from the taskbar and Mission Control on creation.
- Initial placement is relative to the parent; see the platform table in Open a dialog.
There is no alignment or fullScreen on DialogOptions.
Example with global defaults and a one-off override:
runMultiApp(
home: (context, id) => DialogModalLayer(child: MyApp()),
config: MultiAppConfig(
globalDialogOptions: DialogOptions(
modal: true,
size: const Size(520, 400),
isResizable: true,
),
),
);
// Later, in a window:
await openDialog<void>(
(_, __) => const ConfirmDialog(),
parentContext: context,
options: DialogOptions(
title: 'Confirm',
modal: false, // overrides global modal: true for this call only
shellOverrides: ViewShellOverrides(
appearance: AppShellPatch(themeMode: ThemeMode.dark),
),
),
);
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
FutureOr<bool> onWindowClose() {
// The user pressed the close button or closeWindow was called.
// If setPreventClose is true this fires instead of actually closing.
// Return false to abort softCascade for this window.
return true;
}
@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. The callback may return FutureOr<bool>: return true to continue a cascade close, false to abort it (same effect as cancelCascadeClose() for this window's cascade wait).
class _MyPageState extends State<MyPage> with WindowListener {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
MultiViewDesktop.of(context).setPreventClose(true);
});
}
@override
Future<bool> 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.of(context);
win.setPreventClose(false);
await win.closeWindow();
return true;
}
return false; // abort cascade if this window was part of softCascade
}
}
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:
MultiViewDesktop.setCloseMode(CloseMode.softCascade);
| Mode | Behavior |
|---|---|
CloseMode.softCascade |
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.softCascade 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), return false from onWindowClose or call cancelCascadeClose():
@override
Future<bool> onWindowClose() async {
final confirmed = await showUnsavedChangesDialog();
if (!confirmed) {
MultiViewDesktop.of(context).cancelCascadeClose();
return false;
}
return true;
}
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.allWindowViewIds returns a snapshot of all secondary window IDs currently open. allWindowIdsNotifier is a ValueNotifier updated every time a window opens or closes:
ValueListenableBuilder<List<int>>(
valueListenable: MultiViewDesktop.allWindowIdsNotifier,
builder: (context, ids, _) {
return Text('Open secondary windows: ${ids.length}');
},
)
For dialogs, use allDialogViewsIds and allDialogIdsNotifier. See Open a dialog.
Window observers
WindowObserver lets you monitor window and dialog lifecycle from one central place, without adding a WindowListener mixin to every widget. The design mirrors NavigatorObserver in Flutter: extend the class, override only the methods you need, and register instances in MultiAppConfig.observers.
class AppWindowObserver extends WindowObserver {
@override
void onWindowOpened(int viewId, {int? parentViewId}) {
print('window $viewId opened (parent: $parentViewId)');
}
@override
void onWindowClosed(int viewId) {
print('window $viewId closed');
}
@override
void onDialogOpened(int dialogId, {required int parentViewId}) {
print('dialog $dialogId opened (parent window: $parentViewId)');
}
@override
void onDialogClose(int dialogId) {
print('dialog $dialogId closed');
}
@override
void onAnchorChanged(int? previousViewId, int? newViewId) {
print('anchor changed: $previousViewId to $newViewId');
}
@override
void onWindowEvent(int viewId, String eventName) {
print('window $viewId event: $eventName');
}
@override
void onDialogEvent(int dialogId, String eventName) {
print('dialog $dialogId event: $eventName');
}
}
void main() {
runMultiApp(
home: ...,
config: MultiAppConfig(
observers: [AppWindowObserver()],
),
);
}
Multiple observers can be registered at once. All view IDs are public IDs, the same as MultiViewDesktop.getIdByContext, allWindowViewIds, and allDialogViewsIds.
Window callbacks
| Callback | When it fires |
|---|---|
onWindowOpened |
After openWindow completes and the widget tree for the new view is registered. parentViewId is set when parentContext was passed to openWindow, otherwise null. |
onWindowClosed |
After the window is destroyed and its widget tree is disposed. |
onWindowEvent |
For every native event on a window (not a dialog). Fires before matching WindowListener callbacks in that view. |
onAnchorChanged |
When the anchor window changes (automatic promotion or manual setAnchorId). |
Dialog callbacks
Dialog views are not windows in the registry: they use separate observer methods. If you only override window callbacks, dialog open/close and native events on dialogs are silent.
| Callback | When it fires |
|---|---|
onDialogOpened |
After openDialog completes and the dialog widget tree is registered. parentViewId is always the window that called openDialog. |
onDialogClose |
After the dialog is destroyed (closeDialog, native close button, or parent window closed). Does not fire for regular windows; use onWindowClosed for those. |
onDialogEvent |
For every native event on a dialog. Fires before WindowListener in the dialog view (dialogs still use WindowListener mixin methods such as onWindowFocus, not separate dialog-named methods). |
onDialogEvent uses the same eventName strings as onWindowEvent, but dialogs never emit minimize or full-screen events:
eventName |
Dialog | Window |
|---|---|---|
focus, blur |
yes | yes |
resize, resized |
yes | yes |
move, moved |
yes | yes |
close |
yes | yes |
maximize, unmaximize |
no | yes |
minimize, restore |
no | yes |
enter-full-screen, leave-full-screen |
no | yes |
When the parent window closes, child dialogs close first; expect onDialogClose for each dialog, then onWindowClosed for the parent.
Observer vs listener
WindowObserver and WindowListener serve different purposes:
WindowListeneris a mixin onStatein one view. Use it to update UI in that window or dialog.WindowObserveris registered once inMultiAppConfigand receives callbacks for all windows and dialogs. Use it for logging, analytics, or app-wide infrastructure.
Observers are passive: they cannot cancel close or block events.
Application config
MultiAppConfig is passed to runMultiApp once:
runMultiApp(
home: (context, id) => const MyApp(),
config: MultiAppConfig(
generalParams: MultiPlatformParams(
closeMode: CloseMode.softCascade,
enableDynamicAnchor: true,
closeAppAfterLastWindowClosed: true,
animation: ViewAnimationConfig.defaults,
menuItems: [
TaskbarMenuItem(
title: 'Open new window',
onPressed: () => openWindow((ctx, id) => const HomePage()),
),
],
),
macosParams: MacosPlatformParams(
saveLastWindowToReopen: true,
onTaskbarTap: () {
// Dock icon clicked while the app is running.
},
onTerminate: () async {
final allClosed = await MultiViewDesktop.closeApp(closeMode: CloseMode.softCascade);
return allClosed;
},
),
globalWindowOptions: WindowOptions(
size: const Size(1280, 720),
title: 'My App',
),
globalDialogOptions: DialogOptions(modal: false, size: Size(480, 360)),
fileLogParams: const LogParams(enable: true, sizeKb: 2048),
observers: [AppWindowObserver()],
),
);
enableDynamicAnchor: when true, the library automatically tracks which window becomes the "anchor" (the last window visible). The anchor ID is accessible via MultiViewDesktop.getAnchorId().
menuItems: initial taskbar / dock context menu entries (Linux, macOS, and Windows). Replaced entirely by MultiViewDesktop.setMenuItems. Optional iconAsset per item is supported on Windows and macOS; Linux shows the title only.
animation: native open/close fade and optional geometry animation. See Animations.
closeAppAfterLastWindowClosed: when true, quitting after the last window closes terminates the process. Default: true. On macOS, not applied when saveLastWindowToReopen is true.
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. Ignored when using closeApp, onTerminate, or CloseMode.destroy.
onTerminate (macOS): async callback invoked on Cmd+Q and Quit from the menu. Return true to quit the process, false to cancel. Requires applicationShouldTerminate in AppDelegate (see macOS setup).
onTaskbarTap (macOS): called when the user clicks the dock icon. Requires applicationShouldHandleReopen in AppDelegate.
globalWindowOptions: default WindowOptions merged into every openWindow call.
globalDialogOptions: default DialogOptions merged into every openDialog call.
fileLogParams: optional package file logger. See Logging.
observers: list of WindowObserver instances; receives window and dialog lifecycle callbacks. See Window observers for dialog-specific methods (onDialogOpened, onDialogClose, onDialogEvent).
Logging
Diagnostics can be written to mvd.log via MultiAppConfig.fileLogParams (LogParams). Off by default.
runMultiApp(
home: (context, id) => const MyApp(),
config: MultiAppConfig(
fileLogParams: const LogParams(enable: true, sizeKb: 1024),
),
);
| Field | Default | Description |
|---|---|---|
enable |
false |
When true, append package diagnostics to mvd.log. |
sizeKb |
1024 |
Max size of mvd.log in kilobytes. On overflow the file is rotated to mvd.log.prev. |
Log directory (same in debug and release):
- macOS:
~/Library/Caches/multiview_desktop(under the app container when sandboxed) - Windows:
%LOCALAPPDATA%/multiview_desktop/logs - Linux:
${XDG_CACHE_HOME:-~/.cache}/multiview_desktop
API
MultiViewDesktop
Per-window methods are accessed through an instance obtained from a factory constructor:
final win = MultiViewDesktop.of(context);
win.setTitle('My Window');
await win.closeWindow();
// Or by view ID:
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.
appShell -> AppShellController
Shared entry shell for secondary and dialog views. Update through patch or apply. See AppShell.
setGlobalBrightness(Brightness brightness) -> void
Sets the preferred brightness for native chrome on all windows. Does not change Flutter ThemeData; use appShell for that.
allWindowViewIds -> List<int>
Snapshot of public view IDs for all secondary windows currently open.
allWindowIdsNotifier -> ValueNotifier<List<int>>
Live-updating notifier. Fires whenever a window opens or closes.
allDialogViewsIds -> List<int>
Snapshot of public view IDs for all dialogs currently open.
allDialogIdsNotifier -> ValueNotifier<List<int>>
Live-updating notifier. Fires whenever a dialog opens or closes.
Screens (static)
Access via MultiViewDesktop.screen. Display matching mixes physical overlap, DPI, and panel diagonal. DPI alone is not unique.
final screens = MultiViewDesktop.screen.all();
final current = MultiViewDesktop.of(context).display();
final covered = MultiViewDesktop.of(context).setPhysicalBounds(
current.physicalBounds ?? Rect.zero,
);
screen.primary() -> Display
Primary display (first entry in the system screen list).
screen.all() -> List<Display>
Every connected display.
screen.cursorPoint() -> Offset
Cursor position in Flutter logical coordinates.
screen.cursorPhysicalPoint() -> Offset
Cursor position in device pixels. On Windows this is the virtual-desktop point.
screen.resolve({...}) -> Display
Picks the display that best matches the given hints: physicalPoint / physicalBounds, dpi, diagonalMm, then logical point/bounds.
screen.underCursor() -> Display
Display under the cursor, falling back to the primary display.
screen.addListener(ScreenListener listener) -> void
Subscribes to display-added / display-removed.
screen.removeListener(ScreenListener listener) -> void
Removes a previously added screen listener.
Display
Snapshot of a connected monitor. Logical size/position use Flutter coordinates. physicalBounds / physicalWorkArea are device pixels. dpi and physicalWidthMm / physicalHeightMm (and diagonalMm) are used when matching displays; two monitors can share the same DPI.
Identity (instance)
id -> int
The shifted (public) view ID for this instance.
App-wide lifecycle (static)
openWindow(Widget child, {WindowOptions? options, BuildContext? parentContext, AnimationSettings? animation}) -> Future<int>
Opens a new OS window showing child. Returns the view ID. Available as a top-level function; can be called without BuildContext. animation: is a soft override; see Animations.
openDialog<T>(Widget child, {required BuildContext parentContext, DialogOptions? options, AnimationSettings? animation}) -> Future<T?>
Opens a dialog tied to parentContext. Completes when the dialog is closed via closeDialog. Parent must be a window, not another dialog. See Open a dialog.
openDialogEntry<T>(...) -> Future<DialogEntry<T?>>
Same as openDialog, but returns the public view ID immediately together with the result future.
closeApp({CloseMode? closeMode}) -> Future<bool>
Closes all windows using closeMode (or the mode configured in MultiAppConfig). Returns true if every window closed successfully.
setCloseMode(CloseMode closeMode) -> void
Changes the strategy used when the main window close button is pressed.
getCloseMode() -> CloseMode
Returns the currently active close mode.
setAnchorId(int viewId) -> 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)
setForceAnimation(ViewAnimationType type, AnimationSettings animation) -> void
Stages a one-shot force animation for the next matching operation on this view. Runs even if that type is disabled in config. Not used for popups. See Animations.
completeShow() -> void
Finishes a deferred create when WindowOptions.showOnInit / DialogOptions.showOnInit was false. See Deferred show. Do not confuse with show().
closeWindow({AnimationSettings? animation}) -> Future<bool>
Soft-closes this window. If setPreventClose is true, emits onWindowClose instead of destroying the window. Returns true when the window finished closing, or false when the close was cancelled.
closeDialog(dynamic result, AnimationSettings? animation) -> Future<bool>
Closes this dialog and completes the openDialog future on the caller side with result. No effect on regular windows. Returns true when the dialog closed successfully.
isPreventClose() -> bool
Returns whether close is currently blocked for this window.
setPreventClose(bool isPreventClose) -> void
When true, any close attempt (native button or closeWindow) is blocked and onWindowClose fires instead. Set back to false to re-enable.
cancelCascadeClose() -> void
Aborts an in-progress CloseMode.softCascade sequence that is waiting on this window.
Title and appearance (instance)
getTitle() -> String
Returns the native window title.
setTitle(String title) -> void
Changes the native window title shown in the title bar and dock tooltip.
setTitleBarStyle(TitleBarStyle style, {bool closeVisibility = true, bool maximizeVisibility = true, bool minimizeVisibility = true}) -> void
Changes the title-bar style. Pass TitleBarStyle.hidden for a frameless window. The visibility flags control the close / maximize / minimize buttons when the bar is hidden.
getTitleBarStyle() -> ({TitleBarStyle? style, bool? closeVisibility, bool? maximizeVisibility, bool? minimizeVisibility})
Returns the current title-bar style and button visibility.
setAsFrameless() -> void
Removes the native title bar and border entirely.
setBackgroundColor(Color color) -> void
Sets the native window background color behind the Flutter view. Use Colors.transparent for a transparent window.
setBrightness(Brightness brightness) -> void
Sets the preferred appearance of native chrome (light or dark).
setOpacity(double opacity) -> void
Sets window opacity in the range 0.0 (fully transparent) to 1.0 (fully opaque).
getOpacity() -> double
Returns the current window opacity.
hasShadow() -> bool
Returns whether the window draws a native drop shadow.
setHasShadow(bool value) -> void
Enables or disables the native drop shadow. No-op on Linux.
Size and position (instance)
getBounds() -> Rect
Returns the window frame in Flutter logical coordinates (position and size combined).
getPhysicalBounds() -> Rect
Returns the window frame in device pixels (Y-down). Use with Display.physicalBounds on mixed-DPI layouts. Logical coordinates cannot span those layouts reliably.
setPhysicalBounds(Rect rect) -> bool
Moves and resizes the window using device pixels. Does not activate and does not animate. Returns false if rect is empty.
display() -> Display
Display that currently contains most of this window. Matching mixes physical overlap, DPI, and panel diagonal.
getSize() -> Size
Returns the content size in logical pixels.
getPosition() -> Offset
Returns the top-left position of the window.
getMinimumSize() -> Size
Returns the minimum size the user can resize the window to.
getMaximumSize() -> Size
Returns the maximum size the user can resize the window to.
setSize(Size size, {AnimationSettings? animation}) -> Future<bool>
Resizes the window to size in logical pixels. Returns false if size is outside the current min/max.
setPosition(Offset position, {AnimationSettings? animation}) -> Future<void>
Moves the window so its top-left corner is at position. On Wayland (Linux) the compositor may ignore the request silently.
center({AnimationSettings? animation}) -> Future<void>
Centers the window on the screen that contains the largest portion of it.
setAlignment(Alignment alignment, {AnimationSettings? animation}) -> Future<void>
Positions the window using alignment on the display under the cursor. On Wayland (Linux) the compositor may ignore the request silently.
setDialogAlignment(Alignment alignment, {AnimationSettings? animation}) -> Future<void>
Repositions a dialog within its parent window bounds. Regular windows should use setAlignment.
setMinimumSize(Size size) -> bool
Sets the minimum size the user can resize the window to. Returns false while an aspect ratio is locked, or if size exceeds the current maximum.
setMaximumSize(Size size) -> bool
Sets the maximum size the user can resize the window to. Returns false while an aspect ratio is locked, or if size is below the current minimum.
getAspectRatio() -> double
Returns the locked content aspect ratio (width / height). Returns 0 when no ratio is locked.
setAspectRatio(double ratio) -> Future<bool>
Locks the content area to a fixed aspect ratio (width / height) and resizes to match, keeping the current center. Pass 0 to remove the constraint. Returns false if the matching size or tightened min/max cannot be applied. Per-window: each view keeps its own lock and the min/max that were in effect before it.
Visibility and focus (instance)
show() -> void
Shows the window if it was hidden.
hide() -> void
Hides the window without closing it.
isVisible() -> bool
Returns whether the window is currently visible.
focus() -> void
Brings the window to the front and gives it keyboard focus.
blur() -> void
Removes keyboard focus from the window.
isFocused() -> bool
Returns whether this window is the current focused window.
Maximize, minimize, full screen (instance)
isMaximized() -> bool
Returns whether the window is in the maximized state.
maximize({bool vertically = false}) -> void
Maximizes the window. When vertically is true (Windows only), maximizes to half the screen height.
unmaximize() -> void
Restores the window from the maximized state.
isMinimized() -> bool
Returns whether the window is minimized to the dock or taskbar.
minimize() -> void
Minimizes the window.
restore() -> void
Restores the window from the minimized state.
isFullScreen() -> bool
Returns whether the window is in native full-screen mode.
setFullScreen(bool isFullScreen) -> void
Enters or exits native full-screen mode.
Resizability and movability (instance)
isResizable() -> bool
Returns whether the user can resize the window by dragging its edges.
setResizable(bool isResizable) -> void
Enables or disables user resizing.
isMovable() -> bool
Returns whether the window can be moved by dragging the title bar.
setMovable(bool isMovable) -> void
Enables or disables moving the window by dragging. On Linux this maps to setResizable.
isMinimizable() -> bool
Returns whether the minimize button is enabled.
setMinimizable(bool isMinimizable) -> void
Enables or disables the minimize button and action.
isMaximizable() -> bool
Returns whether the maximize / zoom button is enabled.
setMaximizable(bool isMaximizable) -> void
Enables or disables the maximize button and action.
isClosable() -> bool
Returns whether the close button is enabled.
setClosable(bool isClosable) -> void
Enables or disables the close button and native close action.
Always on top and taskbar
isAlwaysOnTop() -> bool (instance)
Returns whether the window floats above normal application windows.
setAlwaysOnTop(bool isAlwaysOnTop) -> void (instance)
Keeps the window above other windows. On Linux depends on compositor support.
isHideAppFromTaskbar() -> bool (static)
Returns whether the application icon is hidden from the dock / taskbar (app-wide).
hideAppFromTaskbar(bool isHideAppFromTaskbar) -> void (static)
Hides or shows the application icon in the dock / taskbar app-wide.
setMenuItems(List<TaskbarMenuItem> items) -> void (static)
Replaces the entire taskbar / dock context menu. Linux (freedesktop .desktop Actions), macOS (dock menu), Windows (taskbar jump list). Optional iconAsset on Windows and macOS; Linux shows the title only.
Initial items can also be set via MultiPlatformParams.menuItems in runMultiApp.
isHideAppTabFromTaskbar() -> bool (instance)
Returns whether this specific window is hidden from the taskbar (Windows / Linux).
hideCurrentAppTabFromTaskbar(bool isHide) -> void (instance)
Hides or shows this window in the taskbar (Windows / Linux).
Drag and resize (instance, used by widgets)
startDragging() -> void
Starts a native window-move drag session. Called automatically by DragToMoveArea.
startResizing(ResizeEdge edge) -> void
Starts a native window-resize drag session from edge. Called automatically by DragToResizeArea.
Mouse events (instance)
setIgnoreMouseEvents(bool ignore, {bool mouseMoveEvents = false}) -> 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() -> ({bool mouseMoveEvents, bool ignore})
Returns the current mouse pass-through state.
popUpWindowMenu() -> void
Shows the native window context menu at the current cursor position.
macOS-specific (instance)
Access via MultiViewDesktop.of(context).macos (or .fromId(id).macos).
final mac = MultiViewDesktop.of(context).macos;
mac.setVisibleOnAllWorkspaces(true);
final onSpace = mac.isOnActiveSpace();
isHideFromCollection() -> bool
Returns whether the window is excluded from Mission Control.
hideFromCollection(bool isHideFromCollection) -> void
Hides or shows the window in Mission Control and Expose.
isVisibleOnAllWorkspaces() -> bool
Returns whether the window is pinned to all Spaces.
setVisibleOnAllWorkspaces(bool visible, {bool visibleOnFullScreen = false}) -> void
Pins or unpins the window across all Spaces.
isOnActiveSpace() -> bool
Returns whether the window is on the currently active Mission Control Space. On Windows / Linux always true.
setBadgeLabel({String? label}) -> void
Sets the dock icon badge text. Pass null to clear the badge.
Progress bar
setProgressBar(double progress) -> bool
Sets the taskbar / dock progress indicator from 0.0 to 1.0. App-wide on Windows. macOS shows progress in the dock.
Entry shell (instance)
patchViewShell(ViewShellOverrides overrides) -> void
Merges overrides into this view's shell configuration. Appearance fields override the global appShell snapshot for this view only. Navigation fields apply only here. See AppShell.
viewShellOverrides -> ViewShellOverrides?
Current per-view overrides, if any.
AppShell
Shared entry shell for secondary windows and dialogs. Access through MultiViewDesktop.appShell.
Secondary content is wrapped in SharedEntryApp, which merges the global registry with per-view ViewShellOverrides and builds MaterialApp, CupertinoApp, or WidgetsApp without duplicating navigation from the main window.
While the main window is open, app-wide fields are copied from its entry widget each frame. You can also call patch or apply from any window, including after the main window was closed.
The main window is not updated by appShell automatically (see Entry shell (AppShell)).
snapshot -> AppShellSnapshot?
Latest app-wide shell settings used for secondary and dialog views.
listenable -> Listenable
Fires when snapshot changes. Safe for ListenableBuilder on the main window if you only read, never patch inside the builder.
patch(AppShellPatch patch) -> void
Merges partial updates into the registry. Rebuilds all secondary and dialog shells.
apply(AppShellSnapshot snapshot) -> void
Replaces the entire registry snapshot.
applyFromMaterialApp(MaterialApp app) -> void
Copies app-wide fields from a MaterialApp into the registry.
applyFromCupertinoApp(CupertinoApp app) -> void
Copies app-wide fields from a CupertinoApp into the registry.
applyFromWidgetsApp(WidgetsApp app) -> void
Copies app-wide fields from a WidgetsApp into the registry.
AppShellPatch
Partial update for the global registry or for ViewShellOverrides.appearance. Fields include theme, darkTheme, themeMode, locale, localizationsDelegates, supportedLocales, shortcuts, and similar. Navigation fields belong in ViewShellOverrides, not here.
ViewShellOverrides
Per-view shell configuration on WindowOptions.shellOverrides or DialogOptions.shellOverrides.
appearance(AppShellPatch?): overrides theme, locale, and other app-wide fields for this view only.- Navigation:
home,routes,routerConfig,navigatorKey, and related fields. Each view has its own navigator or router stack.
Factory: ViewShellOverrides.appearance(AppShellPatch(...)) for appearance-only overrides.
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() -> FutureOr<bool>
Fires when the window is going to close (or when close is blocked by setPreventClose). Return true to continue a cascade close, false to abort. Default implementation returns true.
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.
WindowObserver
Global observer for window and dialog lifecycle. Extend this class and override the callbacks you need. Register instances via MultiAppConfig.observers.
Usage guide with callback tables and event names: Window observers.
All view ID parameters are public (shifted) IDs.
onWindowOpened(int viewId, {int? parentViewId}) -> void
Called after a new OS window has been opened and its widget tree registered.
onWindowClosed(int viewId) -> void
Called after an OS window has been closed and its widget tree disposed.
onDialogOpened(int dialogId, {required int parentViewId}) -> void
Called after a dialog has been opened. parentViewId is the window that called openDialog.
onDialogClose(int dialogId) -> void
Called after a dialog has been closed and its widget tree disposed. Not called for regular windows.
onAnchorChanged(int? previousViewId, int? newViewId) -> void
Called when the anchor window changes.
onWindowEvent(int viewId, String eventName) -> void
Called for every native event on a window. See Window observers for eventName values.
onDialogEvent(int dialogId, String eventName) -> void
Called for every native event on a dialog. Same names as windows except minimize and full-screen events are never sent.
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.
Full field reference: Window options (shared appearance fields plus window-only fields).
Built-in default size: 800x600.
DialogOptions
Initial configuration for a dialog. Passed to openDialog or set as globalDialogOptions in MultiAppConfig.
Full field reference: Dialog options (shared appearance fields plus dialog-only fields).
Built-in default size: 400x300. Default modal: false. Default showOnInit: true. When showOnInit is false, call completeShow() (not show()). See Deferred show.
Dialogs cannot use full-screen mode. Modal dialogs block the parent on all platforms; only macOS keeps them fixed inside the parent window. See Open a dialog.
MultiAppConfig
Passed to runMultiApp once.
generalParams -> MultiPlatformParams
Cross-platform parameters.
closeMode - the CloseMode used when the main window closes. Default: CloseMode.softCascade.
enableDynamicAnchor - when true, automatically tracks the last visible window as the anchor. Default: false.
menuItems - initial taskbar / dock context menu items (TaskbarMenuItem). Default: empty. Replaced at runtime by MultiViewDesktop.setMenuItems. Optional iconAsset per item on Windows and macOS; Linux shows the title only.
animation - native open/close fade and optional geometry animation. Default: ViewAnimationConfig.defaults. See Animations.
closeAppAfterLastWindowClosed - terminate the process after the last window closes. Default: true. On macOS, not applied when saveLastWindowToReopen is true.
macosParams -> MacosPlatformParams
macOS-specific parameters.
saveLastWindowToReopen - restore the last window when the dock icon is clicked after all windows close. Default: true. Ignored for closeApp, onTerminate, and CloseMode.destroy.
onTerminate - async callback on Cmd+Q and Quit from the menu. Return true to terminate, false to cancel. Default: null (quit immediately). Requires applicationShouldTerminate in AppDelegate.
onTaskbarTap - called when the user clicks the dock icon. Requires applicationShouldHandleReopen in AppDelegate. Default: null.
globalWindowOptions -> WindowOptions
Default WindowOptions merged into every new window. Per-window options override these.
globalDialogOptions -> DialogOptions
Default DialogOptions merged into every openDialog call. Per-dialog options override these.
fileLogParams -> LogParams
Optional file logger (enable, sizeKb). Default: disabled. See Logging.
observers -> List<WindowObserver>
List of observers notified on window and dialog lifecycle events. See Window observers.
CloseMode
Controls what happens to other windows when the main window close button is pressed.
softCascade
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.
ViewAnimationConfig
Application-wide native view animation policy. Pass as MultiPlatformParams.animation. Full usage guide: Animations.
ViewAnimationConfig.defaults
Open/close fade for windows, modeless dialogs, and popups. Geometry off. Modal library fade only on Windows; macOS/Linux use native presentation animation.
ViewAnimationConfig.disabled
Everything off.
ViewAnimationConfig.openClose(...)
Open/close fade only. Optional fps, curve, and per-kind durations.
ViewAnimationConfig.geometry({int? fps, Duration duration, Curve curve})
Size/position animation only (setSize, setPosition, setAlignment, center).
ViewAnimationConfig.all(...)
Fade and geometry together.
AnimationSettings
Per-call timing override: duration, curve, fps. Pass as animation: on openWindow, setSize, closeWindow, popup open / close, and similar. Soft overrides apply only when that type is already enabled. Force overrides go through setForceAnimation.
PopupView
Native popup window anchored to a widget. Full usage guide: Open a popup.
Supported on macOS, Windows, and Linux with X11. Disabled on Linux without X11 (Wayland), because popup placement needs client-side positioning.
PopupView(
controller: controller,
positioner: const PopupPositioner(),
builder: (context) => const MyMenu(),
child: trigger,
)
controller -> PopupController
Opens, closes, and holds the popup child for the open session.
builder -> WidgetBuilder
Built once per PopupController.open and reused if this widget remounts.
child -> Widget
Trigger widget the popup is anchored to.
positioner -> PopupPositioner
How the popup is placed relative to child. Defaults to below-left of the trigger, flipping vertically and sliding horizontally to stay on screen.
PopupController
isOpen -> bool
Whether the popup is currently requested open.
open({AnimationSettings? animation}) -> Future<void>
Opens the popup. No-op when already open.
close({AnimationSettings? animation}) -> Future<void>
Closes the popup and drops the native window and cached child.
toggle({AnimationSettings? animation}) -> Future<void>
Toggles open / close.
viewController -> PopupViewController
Opacity, shadow, background color (Windows), and mouse pass-through. Position and size stay with PopupView.
PopupPositioner
parentAnchor / childAnchor -> PopupPositionerAnchor
Attachment points on the trigger and on the popup.
offset -> Offset
Extra translation after aligning the anchors.
constraintAdjustment -> PopupConstraintAdjustment
Flip, slide, or resize when the popup would leave the display.
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 |
DialogModalLayer
Optional overlay on a parent window that dims content while modal dialogs are open. Place it above the main content (often wrapping MaterialApp in home):
DialogModalLayer(child: MaterialApp(home: HomePage()))
Listens to modal dialog state for this window and fades a scrim in and out. Native modal dialogs also block parent input at the OS level; the scrim is visual only.
License
Libraries
- multiview_desktop
- Multi-window support for Flutter desktop.