Getting Started with Flutter Multi-Window: Building Desktop Apps with Multiple Screens
Desktop applications often demand more than a single screen. Think about a sophisticated IDE, a complex design tool, or even just a chat app that pops out conversations into separate windows. For a long time, this kind of multi-window experience was a significant hurdle for Flutter desktop developers.
But no more! Flutter’s flutter_multi_window package has emerged as a powerful solution, allowing you to build truly native, multi-window desktop applications for Windows and macOS. While still officially marked as experimental, it’s remarkably stable for many use cases.
This post will guide you through setting up a multi-window Flutter project, managing individual window lifecycles, and most importantly, enabling seamless communication between your different application windows. Let’s dive in!
Why Multi-Window Matters for Desktop
A single-window interface can feel restrictive on desktop. Users expect:
- Parallel Tasks: Working on multiple documents or views simultaneously.
- Enhanced Productivity: Arranging different parts of an application across multiple monitors.
- Context Switching: Keeping a reference window open while interacting with another.
- Rich Experiences: Complex applications often benefit from dedicated windows for specific functionalities (e.g., a properties panel, a debugger, a separate chat conversation).
Flutter’s multi-window capabilities unlock these possibilities, allowing you to deliver the kind of polished, powerful desktop applications users expect.
Getting Started: Enabling Multi-Window
First things first, you need to add the flutter_multi_window package to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_multi_window: ^0.2.0 # Check for the latest version on pub.dev
Then, run flutter pub get.
The core setup involves a slight modification to your main.dart file (and any other entry points for secondary windows). You need to call enableFlutterMultiWindow before runApp.
Let’s imagine our main application starts in main.dart, and we’ll have secondary windows launched from main_secondary.dart.
main.dart (Your primary application entry point):
import 'package:flutter/material.dart';
import 'package:flutter_multi_window/flutter_multi_window.dart';
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
// This is crucial! Enable multi-window capabilities.
// The 'args' here are command-line arguments, which are used by
// flutter_multi_window to identify secondary windows.
await FlutterMultiWindow.enableFlutterMultiWindow(
args,
// You can optionally provide a callback for when a window is created
// This is useful for initializing services specific to each window.
// Here, we just print the window ID.
onWindowCreated: (window) {
debugPrint('Main window created: ${window.id}');
},
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final List<int> _secondaryWindowIds = [];
String _messageFromOtherWindow = 'No message yet.';
@override
void initState() {
super.initState();
// Set up a method handler to receive messages from other windows
FlutterMultiWindow.setMethodHandler((call) async {
debugPrint('Main window received method call: ${call.method}');
if (call.method == 'messageFromSecondary') {
final Map<String, dynamic> arguments = Map<String, dynamic>.from(call.arguments);
final int senderId = arguments['senderId'];
final String message = arguments['message'];
setState(() {
_messageFromOtherWindow = 'From Window $senderId: "$message"';
});
}
return null;
});
}
Future<void> _openNewWindow() async {
final newWindow = await FlutterMultiWindow.createWindow(
'main_secondary.dart', // This is the entry point for the new window
arguments: {
'initialMessage': 'Hello from the main window!',
'parentWindowId': FlutterMultiWindow.currentWindow.id,
},
);
// You can customize the new window's properties here
await newWindow.setFrame(const Offset(100, 100) & const Size(600, 400));
await newWindow.setTitle('Secondary Window ${newWindow.id}');
await newWindow.show();
setState(() {
_secondaryWindowIds.add(newWindow.id);
});
}
Future<void> _sendMessageToSecondary(int windowId) async {
final currentWindowId = FlutterMultiWindow.currentWindow.id;
await FlutterMultiWindow.invokeMethod(
windowId,
'messageToSecondary',
{'message': 'Hello from Main Window!', 'senderId': currentWindowId},
);
debugPrint('Sent message to window $windowId');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flutter Multi-Window Main App')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Current Window ID: ${FlutterMultiWindow.currentWindow.id}'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _openNewWindow,
child: const Text('Open New Secondary Window'),
),
const SizedBox(height: 40),
Text('Message from secondary: $_messageFromOtherWindow',
style: const TextStyle(fontSize: 16)),
const SizedBox(height: 20),
if (_secondaryWindowIds.isNotEmpty)
Column(
children: [
const Text('Open Secondary Windows:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
..._secondaryWindowIds.map((id) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Window ID: $id'),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () => _sendMessageToSecondary(id),
child: const Text('Send Message'),
),
],
),
)),
],
),
],
),
),
),
);
}
}
Creating Secondary Windows
For each new type of window you want to open, you’ll need a separate Dart file with its own main function. This is how flutter_multi_window knows what code to run in the new process.
main_secondary.dart (Entry point for secondary windows):
import 'package:flutter/material.dart';
import 'package:flutter_multi_window/flutter_multi_window.dart';
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
// Also enable multi-window for secondary windows.
await FlutterMultiWindow.enableFlutterMultiWindow(
args,
onWindowCreated: (window) {
debugPrint('Secondary window created: ${window.id}');
},
);
runApp(const SecondaryApp());
}
class SecondaryApp extends StatefulWidget {
const SecondaryApp({super.key});
@override
State<SecondaryApp> createState() => _SecondaryAppState();
}
class _SecondaryAppState extends State<SecondaryApp> {
String _initialMessage = 'No initial message.';
String _messageFromMainWindow = 'No message yet.';
int? _parentWindowId;
@override
void initState() {
super.initState();
_loadInitialArguments();
// Set up a method handler to receive messages from other windows
FlutterMultiWindow.setMethodHandler((call) async {
debugPrint('Secondary window ${FlutterMultiWindow.currentWindow.id} received method call: ${call.method}');
if (call.method == 'messageToSecondary') {
final Map<String, dynamic> arguments = Map<String, dynamic>.from(call.arguments);
final int senderId = arguments['senderId'];
final String message = arguments['message'];
setState(() {
_messageFromMainWindow = 'From Window $senderId: "$message"';
});
}
return null;
});
// Optional: Listen for when this window is about to close
FlutterMultiWindow.currentWindow.onClose().listen((_) {
debugPrint('Window ${FlutterMultiWindow.currentWindow.id} is closing!');
// Perform any cleanup here
if (_parentWindowId != null) {
FlutterMultiWindow.invokeMethod(
_parentWindowId!,
'secondaryWindowClosed',
{'closedWindowId': FlutterMultiWindow.currentWindow.id},
);
}
});
}
void _loadInitialArguments() {
final arguments = FlutterMultiWindow.currentWindow.arguments;
if (arguments != null && arguments is Map<String, dynamic>) {
setState(() {
_initialMessage = arguments['initialMessage'] ?? _initialMessage;
_parentWindowId = arguments['parentWindowId'];
});
}
}
Future<void> _sendMessageToMain() async {
if (_parentWindowId == null) {
debugPrint('Parent window ID not available to send message.');
return;
}
final currentWindowId = FlutterMultiWindow.currentWindow.id;
await FlutterMultiWindow.invokeMethod(
_parentWindowId!,
'messageFromSecondary',
{'message': 'Hello from Secondary Window!', 'senderId': currentWindowId},
);
debugPrint('Sent message from window $currentWindowId to main window $_parentWindowId');
}
Future<void> _closeThisWindow() async {
await FlutterMultiWindow.currentWindow.close();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Secondary Window ${FlutterMultiWindow.currentWindow.id}')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Current Window ID: ${FlutterMultiWindow.currentWindow.id}'),
const SizedBox(height: 20),
Text('Initial Message: $_initialMessage'),
const SizedBox(height: 20),
Text('Message from main: $_messageFromMainWindow',
style: const TextStyle(fontSize: 16)),
const SizedBox(height: 40),
ElevatedButton(
onPressed: _sendMessageToMain,
child: const Text('Send Message to Main Window'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _closeThisWindow,
child: const Text('Close This Window'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
],
),
),
),
);
}
}
To run this example:
- Create
main.dartandmain_secondary.dartfiles in yourlibfolder. - Make sure
flutter_multi_windowis in yourpubspec.yaml. - Run your application:
flutter run -d windowsorflutter run -d macos.
You’ll see your main window appear. Click the “Open New Secondary Window” button, and a new window will pop up. You can open multiple secondary windows!
Understanding Window Lifecycle and Management
Each window created by flutter_multi_window is essentially an independent Flutter engine process. This means:
- Independent State: Each window manages its own widget tree and state.
- Separate Event Loops: UI updates in one window don’t block another.
Key lifecycle aspects:
FlutterMultiWindow.createWindow(entrypoint, {arguments}): This is how you launch a new window.entrypoint: The Dart file (e.g.,main_secondary.dart) containing themainfunction for the new window.arguments: AMap<String, dynamic>to pass initial data to the new window. This is how the new window receives context (like its parent’s ID or initial data).
window.setFrame(),window.setTitle(),window.show(): After creating a window, you get aWindowobject back. Use this to control its appearance and visibility.FlutterMultiWindow.currentWindow.arguments: In a secondary window’smainorinitState, you can access the arguments passed during its creation.FlutterMultiWindow.currentWindow.id: Every window has a unique integer ID. This is crucial for targeted communication.window.close(): Programmatically closes a specific window.window.onClose().listen(...): You can subscribe to a stream to be notified when a window is about to close (either by user action or programmatic closure). This is the perfect place for cleanup or notifying other windows.
Important Note on Closing: When a secondary window closes, its associated windowId might still be in your main window’s list. You’ll need to implement logic to remove it. You could send a message from the closing secondary window to the main window (as demonstrated in the onClose listener in main_secondary.dart).
Inter-Window Communication
This is where the real power lies. flutter_multi_window provides a simple MethodChannel-like mechanism to send data between windows.
-
Sending a Message to a Specific Window: Use
FlutterMultiWindow.invokeMethod(targetWindowId, methodName, [arguments]).targetWindowId: The integer ID of the window you want to send the message to.methodName: A string identifier for the message (e.g.,'updateCount','messageFromSecondary').arguments: An optionaldynamicobject (often aMap<String, dynamic>) containing the data you want to send.
// From main window, sending to a secondary window with ID 123 FlutterMultiWindow.invokeMethod( 123, 'messageToSecondary', {'message': 'Hello from Main!', 'senderId': FlutterMultiWindow.currentWindow.id}, ); -
Receiving Messages in Any Window: Each window that expects to receive messages must set up a method handler using
FlutterMultiWindow.setMethodHandler(). This handler is called whenever any other window sends a message to this window.// In a StatefulWidget's initState (or anywhere else appropriate) FlutterMultiWindow.setMethodHandler((call) async { if (call.method == 'messageToSecondary') { final Map<String, dynamic> arguments = Map<String, dynamic>.from(call.arguments); final int senderId = arguments['senderId']; final String message = arguments['message']; debugPrint('Received message from window $senderId: $message'); // Update UI or state based on the message } else if (call.method == 'secondaryWindowClosed') { final int closedWindowId = call.arguments['closedWindowId']; setState(() { _secondaryWindowIds.remove(closedWindowId); // Remove from our list }); } return null; // Always return null or a Future<dynamic> });Notice that the
setMethodHandlercallback does not receive the sender’s window ID directly. If you need to know who sent the message, the sender must include itsFlutterMultiWindow.currentWindow.idin theargumentsmap, as shown in the example above.
Common Considerations & Best Practices
- State Management: While
flutter_multi_windowhandles window creation and communication, it doesn’t solve global state management across independent processes. For shared state (e.g., user preferences, database connections, application-wide settings), you’ll need:- Persistent Storage: SQLite, Hive, shared preferences, etc., for data that needs to persist even if windows close.
- Message Passing: For transient state updates, use the
invokeMethod/setMethodHandlerpattern to synchronize relevant data between windows. - Singletons/Service Locators (carefully): If certain services must be truly global and shared, you might need to ensure they are initialized consistently in each window’s
mainfunction, or manage them via persistent storage that each window can access.
- Performance: Each window is a separate Flutter engine instance, meaning separate memory and CPU usage. While Flutter is efficient, opening dozens of complex windows might impact performance on lower-end machines. Be mindful of resource consumption.
- User Experience: Design your multi-window application with user expectations in mind.
- How do windows relate to each other?
- What happens when the main window closes? (Typically, all secondary windows should also close unless explicitly designed otherwise).
- How do users switch between windows? (Standard OS mechanisms usually suffice).
- Error Handling: Always consider what happens if a target window ID is invalid or a message isn’t handled.
- The “Experimental” Tag: While
flutter_multi_windowis robust, keep an eye on its development. Breaking changes are possible, though less frequent as it matures.
Conclusion
Flutter’s flutter_multi_window package is a game-changer for desktop development, empowering you to build rich, multi-screen experiences on Windows and macOS. By understanding how to enable multi-window, manage window lifecycles, and implement robust inter-window communication, you’re well on your way to creating powerful and intuitive desktop applications that truly leverage the platform.
Go ahead, give it a try, and elevate your Flutter desktop apps to the next level! Happy coding!
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Building Fluid & Interactive UIs in Flutter: Beyond Basic Animations with Custom Painters and Game-Inspired Techniques
This post will guide developers through creating highly dynamic and visually rich user interfaces using advanced Flutter techniques like CustomPainter, TickerProviderStateMixin, and even drawing inspiration from game development libraries like Flame for effects. We'll explore how to achieve smooth, interactive animations and reactive UIs that feel truly "liquid" without necessarily building a game.
Flutter Secrets: Best Practices for Storing API Keys and Sensitive Data Securely
Learn the robust methods for safeguarding API keys and other sensitive information in your Flutter applications across various platforms. This guide covers compile-time environment variables, native secret storage mechanisms, and secure backend integration to prevent exposure in code or during deployment.
Mastering Responsive & Adaptive Layouts in Flutter: Beyond `MediaQuery`
This post will guide developers through building truly adaptive Flutter UIs that seamlessly adjust to different screen sizes, orientations, and platforms. We'll cover advanced techniques using `LayoutBuilder`, `CustomMultiChildLayout`, and `Breakpoints` to create flexible, maintainable layouts, moving beyond basic `MediaQuery` checks.