Flutter's Native Desktop Multi-Window API Lands on Master
After all these years, the official team has finally dedicated a separate discussion to the multi-window API. Two years ago, Canonical took over the development and maintenance of Flutter's PC side, and it has been pushing multi-window support forward ever since. The current completed design revolves around five window types: regular, dialog, tooltip, popup, and satellite windows.
The regular window needs no introduction; it's the default window type used in most multi-window scenarios.
Next is the Popup window. Popups primarily provide functionality similar to standalone window menus. They are child windows that can receive input focus, and users can use arrow keys to switch selected targets within a dropdown menu. Additionally, the system internally forces Popup windows to remain visible, preventing them from being clipped off-screen due to their popup position.
Then there's the Tooltip. It's very similar to popup windows, but the difference is that tooltips do not have input focus. They are generally used to display hints, such as showing a control's function when the mouse hovers over it.
Dialog is also very common. It's a child window that comes in modal and non-modal forms. When a dialog is displayed modally within another window, the parent window is prevented from gaining focus until the dialog window is closed.
Satellite windows are another common auxiliary popup type. They can maintain their position relative to a parent window, following along when the parent window moves or resizes. Satellites also have docking capabilities, meaning they can transition from a floating Satellite window to being embedded inside the main window.
These window types currently exist within a window hierarchy. For example, an app can set a main window (a regular window) at its root, then nest Popup windows and Dialogs beneath it, and a dialog can further nest a ToolTip inside.
These multi-window APIs are already available on the master channel. I've been using them for a while myself, and there's really nothing wrong with using the main branch:
flutter channel main
flutter upgrade
flutter config --enable-windowing
After configuration, you can create native windows. To create a window, you first need to create a WindowController. This controller primarily interacts with the underlying platform to create and update windows:
final controller = WindowController(
title: 'My Application',
size: const Size(800, 600),
);
The controller receives the initial configuration for the window, such as size and title. Each window type has its own window controller. For example, to create a dialog window:
final dialogController = DialogWindowController(
title: 'My Dialog',
size: const Size(400, 300),
parent: parentController,
);
Unlike a regular window,
DialogWindowControllerhas an optional parent window controller.
Once the controller is created, it can be used directly to modify the window later. For example, we can change the title, size, and destroy the previously created regular window:
controller.setTitle('Hello, world!');
controller.setSize(const Size.square(1000));
controller.destroy();
Next is rendering content to the window. This also requires passing the controller and the content to be rendered to the Window widget:
Widget build(BuildContext context) {
return Window(
controller: controller,
child: MyPage(),
);
}
Each window type has its corresponding widget, for example:
- Use the
Windowwidget for regular windows - Use the
DialogWindowwidget for dialog windows
Because all windows reside in the same widget tree, state can actually be shared across windows.
If you need to listen for window events, such as receiving window close notifications, there are currently two ways to get window state: through WindowControllerDelegate or WindowScope. For example:
// Create the class first...
class MyWindowDelegate with WindowControllerDelegate {
@override
void onWindowDestroyed() {
super.onWindowDestroyed();
ServicesBinding.instance.exitApplication(AppExitType.required);
}
}
// and then pass it to the controller constructor.
final controller = WindowController(
title: 'My Application',
size: const Size(800, 600),
delegate: MyWindowDelegate(),
);
You can also achieve similar listening through WindowScope. You can access the Scope via WindowScope.of and then get the corresponding state:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = WindowScope.titleOf(context);
// ... do something with the window title
}
}
The entire demo looks something like this:
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: implementation_imports
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/src/widgets/_window.dart';
import 'package:flutter/widgets.dart';
/// Exits the application when the user closes the window.
class ExitOnCloseDelegate with WindowControllerDelegate {
@override
void onWindowCloseRequested(WindowController controller) {
ServicesBinding.instance.exitApplication(AppExitType.required);
}
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
runWidget(const HelloWindow());
}
/// Displays a window and owns its [WindowController].
class HelloWindow extends StatefulWidget {
const HelloWindow({super.key});
@override
State<HelloWindow> createState() => _HelloWindowState();
}
class _HelloWindowState extends State<HelloWindow> {
final WindowController _controller = WindowController(
size: const Size(600, 400),
title: 'MyApp',
delegate: ExitOnCloseDelegate(),
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Window(
controller: _controller,
child: const Directionality(
textDirection: TextDirection.ltr,
child: ColoredBox(
color: Color(0xFFFFFFFF),
child: Center(
child: Text(
'Hello, Window',
style: TextStyle(color: Color(0xFF000000), fontSize: 24),
),
),
),
),
);
}
}
One thing to note is that multi-window apps no longer use runApp; you need to use runWidget instead:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runWidget(const MultiWindowApp());
}
Overall, from my experience, Flutter's multi-window support is already usable directly. Although there are minor issues, they don't significantly affect the main workflow. At least it works on Windows and macOS; I haven't tested it on Linux.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Not bad, not bad. Previously using Flutter multi-instance, there were all kinds of communication and performance issues.