← Back to posts Cover image for Getting Started with Flutter Multi-Window: Building Desktop Apps with Multiple Screens

Getting Started with Flutter Multi-Window: Building Desktop Apps with Multiple Screens

· 10 min read
Chris
By Chris

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:

  1. Create main.dart and main_secondary.dart files in your lib folder.
  2. Make sure flutter_multi_window is in your pubspec.yaml.
  3. Run your application: flutter run -d windows or flutter 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 the main function for the new window.
    • arguments: A Map<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 a Window object back. Use this to control its appearance and visibility.
  • FlutterMultiWindow.currentWindow.arguments: In a secondary window’s main or initState, 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.

  1. 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 optional dynamic object (often a Map<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},
    );
  2. 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 setMethodHandler callback does not receive the sender’s window ID directly. If you need to know who sent the message, the sender must include its FlutterMultiWindow.currentWindow.id in the arguments map, as shown in the example above.

Common Considerations & Best Practices

  • State Management: While flutter_multi_window handles 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/setMethodHandler pattern 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 main function, 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_window is 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

Cover image for Building Fluid & Interactive UIs in Flutter: Beyond Basic Animations with Custom Painters and Game-Inspired Techniques

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.

Cover image for Mastering Responsive & Adaptive Layouts in Flutter: Beyond `MediaQuery`

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.