Handling Desktop-Specific Features Like Window Management and Menus in Flutter

Flutter’s versatility extends beyond mobile, allowing you to build applications for the web, desktop, and embedded systems using a single codebase. However, when targeting desktop platforms (Windows, macOS, Linux), you’ll often need to handle desktop-specific features like window management, native menus, and system-level interactions. This blog post will guide you through implementing these features effectively in Flutter desktop applications.

Understanding Desktop-Specific Features

Desktop applications require interactions and functionalities that differ from mobile apps. Key desktop features include:

  • Window Management: Resizing, minimizing, maximizing, and closing the application window.
  • Native Menus: Integrating standard desktop menus (File, Edit, View, etc.) for common actions.
  • System Tray Integration: Minimizing the app to the system tray for background tasks.
  • File Handling: Opening and saving files using native dialogs.
  • Clipboard Management: Copying and pasting data to and from the system clipboard.

Setting Up a Flutter Desktop Project

To get started with Flutter desktop development, ensure you have the necessary configurations for your target platform.


flutter create my_desktop_app
cd my_desktop_app
flutter config --enable-windows-desktop  # For Windows
flutter config --enable-macos-desktop    # For macOS
flutter config --enable-linux-desktop    # For Linux

Run your app using:


flutter run -d windows  # Or macos/linux

Implementing Window Management

For window management in Flutter, you can use the window_manager package, which provides functionalities for controlling the application window.

Step 1: Add window_manager Dependency

Include the window_manager package in your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  window_manager: ^0.3.7 # Use the latest version

Run flutter pub get to install the package.

Step 2: Implement Window Management Logic

Use the WindowManager class to manage the window in your Flutter app:


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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await windowManager.ensureInitialized();

  WindowOptions windowOptions = const WindowOptions(
    size: Size(800, 600),
    center: true,
    backgroundColor: Colors.transparent,
    skipTaskbar: false,
    titleBarStyle: TitleBarStyle.normal,
  );

  windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
    await windowManager.focus();
  });

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Desktop App'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ElevatedButton(
                onPressed: () async {
                  await windowManager.minimize();
                },
                child: const Text('Minimize Window'),
              ),
              ElevatedButton(
                onPressed: () async {
                  await windowManager.maximize();
                },
                child: const Text('Maximize Window'),
              ),
              ElevatedButton(
                onPressed: () async {
                  await windowManager.setSize(const Size(1024, 768));
                },
                child: const Text('Set Window Size'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

In this example:

  • windowManager.ensureInitialized() ensures the window manager is initialized.
  • WindowOptions defines initial window settings such as size and center position.
  • Buttons are added to minimize, maximize, and set the window size.

Adding Native Menus

Integrating native menus can significantly enhance the user experience on desktop platforms. The menubar package facilitates adding platform-specific menus.

Step 1: Add menubar Dependency

Add the menubar package to your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  menubar: ^1.0.3 # Use the latest version

Run flutter pub get.

Step 2: Implement Native Menus

Implement the native menus using MenuBar:


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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  String _status = 'Ready.';

  @override
  void initState() {
    super.initState();
    setMenubar();
  }

  void setMenubar() {
    setApplicationMenu([
      Submenu(label: 'File', children: [
        MenuItem(
            label: 'Open',
            onClicked: () {
              setState(() {
                _status = 'Open menu clicked.';
              });
            }),
        MenuItem(
            label: 'Save',
            onClicked: () {
              setState(() {
                _status = 'Save menu clicked.';
              });
            }),
        MenuDivider(),
        MenuItem(
            label: 'Exit',
            onClicked: () {
              setState(() {
                _status = 'Exit menu clicked.';
              });
              // You might want to close the app here
            }),
      ]),
      Submenu(label: 'Edit', children: [
        MenuItem(label: 'Copy', onClicked: () {}),
        MenuItem(label: 'Paste', onClicked: () {}),
      ]),
      Submenu(label: 'View', children: [
        MenuItem(
            label: 'Toggle Fullscreen',
            onClicked: () async {
              // Handle toggle fullscreen here
            }),
      ]),
    ]);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Desktop App'),
      ),
      body: Center(
        child: Text(_status),
      ),
    );
  }
}

Key points:

  • setApplicationMenu defines the structure of the menu.
  • Submenu creates a submenu with a label and children.
  • MenuItem creates a menu item with a label and onClicked handler.

Handling System Tray Integration

To minimize your application to the system tray, you can use the system_tray package.

Step 1: Add system_tray Dependency

Include the system_tray package in your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  system_tray: ^3.1.0 # Use the latest version

Run flutter pub get.

Step 2: Implement System Tray Logic

Integrate system tray functionalities into your Flutter app:


import 'package:flutter/material.dart';
import 'package:system_tray/system_tray.dart';
import 'dart:io' show Platform;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final SystemTray systemTray = SystemTray();

  // Define menu entries for system tray
  final List menuEntries = [
    MenuItem(label: 'Show', onClicked: () {
      // Handle show window here
    }),
    MenuItem(label: 'Exit', onClicked: () {
      // Handle exit application here
    }),
  ];

  // Initialize the system tray
  await systemTray.initSystemTray(
    title: "My Flutter App",
    iconPath: Platform.isWindows ? "assets/app_icon.ico" : "assets/app_icon.png",
  );

  // Set the context menu for system tray
  await systemTray.setContextMenu(menuEntries);

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Desktop App'),
        ),
        body: const Center(
          child: Text('App running in system tray.'),
        ),
      ),
    );
  }
}

In this example:

  • SystemTray initializes the system tray.
  • initSystemTray sets the title and icon path for the tray icon.
  • setContextMenu sets the context menu with entries like “Show” and “Exit.”

Handling File Operations

Desktop apps often require opening and saving files. The file_selector package simplifies this process.

Step 1: Add file_selector Dependency

Include the file_selector package in your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  file_selector: ^0.9.2 # Use the latest version

Run flutter pub get.

Step 2: Implement File Handling Logic

Implement file opening and saving operations:


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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  String _filePath = 'No file selected';

  Future<void> _openFile() async {
    final XFile? pickedFile = await openFile();
    if (pickedFile != null) {
      setState(() {
        _filePath = pickedFile.path;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Desktop App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Selected File: $_filePath'),
            ElevatedButton(
              onPressed: _openFile,
              child: const Text('Open File'),
            ),
          ],
        ),
      ),
    );
  }
}

Explanation:

  • openFile() displays the native file selection dialog.
  • XFile represents the selected file.

Clipboard Management

Managing the system clipboard is crucial for data exchange in desktop apps. Use the clipboard package for this.

Step 1: Add clipboard Dependency

Add the clipboard package to your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  clipboard: ^0.1.3 # Use the latest version

Run flutter pub get.

Step 2: Implement Clipboard Logic

Implement copy and paste functionalities:


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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  String _clipboardText = '';
  final TextEditingController _textController = TextEditingController();

  Future<void> _copyToClipboard(String text) async {
    await Clipboard.setData(ClipboardData(text: text));
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Text copied to clipboard')));
  }

  Future<void> _pasteFromClipboard() async {
    final data = await Clipboard.getData('text/plain');
    setState(() {
      _clipboardText = data?.text ?? 'Clipboard is empty';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Desktop App'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _textController,
              decoration: const InputDecoration(labelText: 'Enter text'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => _copyToClipboard(_textController.text),
              child: const Text('Copy to Clipboard'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _pasteFromClipboard,
              child: const Text('Paste from Clipboard'),
            ),
            const SizedBox(height: 20),
            Text('Pasted Text: $_clipboardText'),
          ],
        ),
      ),
    );
  }
}

Conclusion

Handling desktop-specific features in Flutter enhances user experience and integrates seamlessly with desktop environments. By utilizing packages such as window_manager, menubar, system_tray, file_selector, and clipboard, you can provide essential desktop functionalities in your Flutter applications. Each feature contributes to making your app feel native and professional on desktop platforms.