← Back to posts Cover image for Unlocking Flutter Desktop: Building Multi-Window Experiences with the Experimental API

Unlocking Flutter Desktop: Building Multi-Window Experiences with the Experimental API

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Beyond the Single Window: Building Multi-Window Desktop Apps with Flutter

For years, Flutter desktop developers faced a significant limitation: the single-window paradigm. While perfect for mobile, this constraint felt artificial on desktop, where users expect the power to open multiple views, pop out panels, or have floating toolbars. Building a complex desktop application often meant resorting to platform channels and native code, adding significant complexity.

Thankfully, the experimental Desktop Windowing API is changing the game. It allows you to spawn and manage multiple top-level windows from a single Flutter isolate, enabling true multi-window experiences. Imagine a video editor with a detached preview pane, a trading app with a separate live ticker window, or a developer tool with a floating debug console. This API makes it possible.

Setting the Stage: Enabling the API

Before diving into code, you must enable the experimental API. This requires modifying your application’s main entry point and build configuration.

1. Update main.dart: Your main function must be annotated and use the correct initialization.

// main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

// This annotation is required for multi-window support on desktop.
@pragma('vm:entry-point')
void main(List<String> args) {
  // Check if this is the main window or a secondary window.
  // The main window's arguments will be empty or contain a specific token.
  if (kIsWeb) {
    // Handle web if needed.
    runApp(const MyApp());
  } else {
    // Determine which window this is based on arguments.
    if (args.contains('settings_window')) {
      runApp(MaterialApp(home: SettingsScreen()));
    } else {
      // This is the primary application window.
      runApp(const MyApp());
    }
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Multi-Window Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const WindowManagerScreen(),
    );
  }
}

2. Modify Platform-Specific Launchers: For Windows (windows/runner/main.cpp), ensure you are using the correct flutter::Window initialization that supports multiple windows. The Flutter template for the experimental API handles this, but if you’re adding it to an existing project, you may need to regenerate or update the runner.

Creating Your First Secondary Window

The core of the API is the DesktopWindowController. Let’s create a simple window manager that can open a secondary settings window.

// window_manager.dart
import 'dart:ui' as ui; // Import dart:ui for window-related APIs.

class WindowManager {
  static Future<void> openSettingsWindow() async {
    try {
      // This is a conceptual example based on the API's direction.
      final controller = await ui.DesktopWindowController.createWindow(
        arguments: 'settings_window', // Unique identifier for the window.
      );
    } catch (e) {
      print('Failed to open window: $e');
    }
  }
}

Sharing Data Between Windows: The Isolate Advantage

Since all windows run in the same Dart isolate, sharing state is remarkably straightforward. You don’t need complex IPC (Inter-Process Communication). A simple state management solution like ValueNotifier, ChangeNotifier, or Riverpod works globally.

Here’s an example using a ChangeNotifier to sync a theme preference:

// shared_theme_model.dart
import 'package:flutter/material.dart';

class SharedThemeModel extends ChangeNotifier {
  bool _isDarkMode = false;

  bool get isDarkMode => _isDarkMode;

  set isDarkMode(bool value) {
    _isDarkMode = value;
    notifyListeners(); // This will notify listeners in ALL windows.
  }
}

// In your main function, initialize this as a global provider or singleton.
final sharedTheme = SharedThemeModel();

// In any window's widget:
@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: sharedTheme,
    builder: (context, child) {
      return MaterialApp(
        theme: sharedTheme.isDarkMode
            ? ThemeData.dark()
            : ThemeData.light(),
        home: MyHomePage(),
      );
    },
  );
}

// A button in any window can update it for all:
ElevatedButton(
  onPressed: () => sharedTheme.isDarkMode = !sharedTheme.isDarkMode,
  child: const Text('Toggle Theme'),
)

Common Pitfalls and Best Practices

  1. Window Lifecycle: Secondary windows are independent. Closing the main window does not automatically close others. You must implement logic to broadcast shutdown commands or check for the main isolate’s status.
  2. Platform-Specific Bugs: This is an experimental API. Be prepared for platform inconsistencies, especially around window focus, z-ordering (always-on-top), and taskbar representation. Test thoroughly on each target OS (Windows, macOS, Linux).
  3. Argument Parsing: The mechanism for passing initial arguments to a new window can be clunky. Keep your argument strings simple and well-documented.
  4. Performance: While sharing an isolate is great for state, remember that heavy computation in one window can block the UI in others. Offload intensive work to isolates or background threads when necessary.

A Practical Example: A Floating Note-Taker

Let’s build a quick proof-of-concept: a main app that can spawn floating, always-on-top note windows.

// In the main window's widget:
import 'dart:ui' as ui;

FloatingActionButton(
  onPressed: () async {
    await ui.DesktopWindowController.createWindow(
      arguments: 'note_window_${DateTime.now().millisecondsSinceEpoch}',
    );
  },
  child: const Icon(Icons.note_add),
);

// In main.dart, handle the note window:
@pragma('vm:entry-point')
void main(List<String> args) {
  if (args.isNotEmpty && args.first.startsWith('note_window')) {
    runApp(MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Quick Note')),
        body: const Padding(
          padding: EdgeInsets.all(16.0),
          child: TextField(
            maxLines: null,
            decoration: InputDecoration(
              border: InputBorder.none,
              hintText: 'Start typing...',
            ),
          ),
        ),
      ),
    ));
  } else {
    runApp(const PrimaryApp());
  }
}

The Desktop Windowing API, though still experimental, unlocks the native multi-window patterns that desktop users demand. By managing windows and sharing data within a single isolate, Flutter developers can now build truly professional, complex desktop applications.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.

Cover image for Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.

Cover image for Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.