Back to Posts

Flutter for Desktop Apps

11 min read

Flutter's support for desktop platforms (Windows, macOS, and Linux) enables developers to create beautiful, native-feeling applications with a single codebase. This guide will walk you through everything you need to know about building desktop applications with Flutter.

Getting Started with Desktop Development

1. Setup and Requirements

flutter config --enable-windows-desktop
flutter config --enable-macos-desktop
flutter config --enable-linux-desktop

flutter create --platforms=windows,macos,linux my_desktop_app

2. Platform-Specific Configuration

Windows

<!-- windows/runner/main.cpp -->
#include <flutter/dart_project.h>
#include <flutter/flutter_window.h>
#include <flutter/standard_method_codec.h>
#include <memory>

int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE prev,
                      wchar_t *command_line, int show_command) {
  flutter::DartProject project(L"data");
  std::vector<std::string> command_line_arguments =
      GetCommandLineArguments();
  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
  
  flutter::FlutterWindow window(project);
  window.SetTitle(L"My Desktop App");
  window.SetIcon("assets/icon.ico");
  window.Run();
  return 0;
}

macOS

// macos/Runner/AppDelegate.swift
import Cocoa
import FlutterMacOS

class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}

Linux

// linux/my_application.cc
#include "my_application.h"

int main(int argc, char** argv) {
  g_autoptr(MyApplication) app = my_application_new();
  return g_application_run(G_APPLICATION(app), argc, argv);
}

Desktop-Specific Features

1. Window Management

import 'package:window_manager/window_manager.dart';

class MainWindow extends StatefulWidget {
  @override
  _MainWindowState createState() => _MainWindowState();
}

class _MainWindowState extends State<MainWindow> with WindowListener {
  @override
  void initState() {
    super.initState();
    windowManager.addListener(this);
    _init();
  }

  Future<void> _init() async {
    await windowManager.setMinimumSize(const Size(800, 600));
    await windowManager.setTitle('My Desktop App');
    await windowManager.center();
  }

  @override
  void onWindowClose() async {
    bool isPreventClose = await windowManager.isPreventClose();
    if (isPreventClose) {
      showDialog(
        context: context,
        builder: (_) {
          return AlertDialog(
            title: Text('Are you sure?'),
            content: Text('Unsaved changes will be lost.'),
            actions: [
              TextButton(
                child: Text('No'),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
              TextButton(
                child: Text('Yes'),
                onPressed: () {
                  Navigator.of(context).pop();
                  windowManager.destroy();
                },
              ),
            ],
          );
        },
      );
    }
  }
}

2. Menu Bar Integration

import 'package:menubar/menubar.dart';

class MenuBarExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Menu Bar Example'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            setApplicationMenu([
              Submenu(label: 'File', children: [
                MenuItem(
                  label: 'New',
                  onClicked: () => print('New clicked'),
                ),
                MenuItem(
                  label: 'Open',
                  onClicked: () => print('Open clicked'),
                ),
                MenuDivider(),
                MenuItem(
                  label: 'Exit',
                  onClicked: () => windowManager.close(),
                ),
              ]),
              Submenu(label: 'Edit', children: [
                MenuItem(
                  label: 'Cut',
                  onClicked: () => print('Cut clicked'),
                ),
                MenuItem(
                  label: 'Copy',
                  onClicked: () => print('Copy clicked'),
                ),
                MenuItem(
                  label: 'Paste',
                  onClicked: () => print('Paste clicked'),
                ),
              ]),
            ]);
          },
          child: Text('Show Menu Bar'),
        ),
      ),
    );
  }
}

3. System Tray Integration

import 'package:tray_manager/tray_manager.dart';

class SystemTrayExample extends StatefulWidget {
  @override
  _SystemTrayExampleState createState() => _SystemTrayExampleState();
}

class _SystemTrayExampleState extends State<SystemTrayExample> with TrayListener {
  @override
  void initState() {
    super.initState();
    trayManager.addListener(this);
    _init();
  }

  Future<void> _init() async {
    await trayManager.setIcon(
      'assets/icon.png',
    );
    await trayManager.setToolTip('My Desktop App');
    await trayManager.setContextMenu([
      MenuItem(
        label: 'Show',
        onClicked: () => windowManager.show(),
      ),
      MenuItem(
        label: 'Hide',
        onClicked: () => windowManager.hide(),
      ),
      MenuDivider(),
      MenuItem(
        label: 'Exit',
        onClicked: () => windowManager.destroy(),
      ),
    ]);
  }

  @override
  void onTrayIconMouseDown() {
    windowManager.show();
  }
}

Platform-Specific Considerations

1. File System Access

import 'package:file_selector/file_selector.dart';

class FileSystemExample extends StatelessWidget {
  Future<void> _openFile() async {
    final typeGroup = XTypeGroup(
      label: 'images',
      extensions: ['jpg', 'png'],
    );
    final file = await openFile(
      acceptedTypeGroups: [typeGroup],
    );
    if (file != null) {
      print('Selected file: ${file.name}');
    }
  }

  Future<void> _saveFile() async {
    final path = await getSavePath();
    if (path != null) {
      final file = File(path);
      await file.writeAsString('Hello, World!');
    }
  }
}

2. Native Integration

import 'package:window_size/window_size.dart';

class NativeIntegrationExample extends StatelessWidget {
  Future<void> _getScreenInfo() async {
    final screen = await getCurrentScreen();
    print('Screen size: ${screen?.frame.width}x${screen?.frame.height}');
  }

  Future<void> _setWindowPosition() async {
    await setWindowFrame(Rect.fromLTWH(100, 100, 800, 600));
  }
}

Best Practices

1. Responsive Design

class ResponsiveLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth > 1200) {
          return DesktopLayout();
        } else if (constraints.maxWidth > 800) {
          return TabletLayout();
        } else {
          return MobileLayout();
        }
      },
    );
  }
}

2. Performance Optimization

class PerformanceOptimizedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: const Text('Optimized Widget'),
    );
  }
}

Common Issues and Solutions

1. Window Management

class WindowManagementExample extends StatefulWidget {
  @override
  _WindowManagementExampleState createState() => _WindowManagementExampleState();
}

class _WindowManagementExampleState extends State<WindowManagementExample> {
  Future<void> _handleWindowState() async {
    // Check if window is minimized
    final isMinimized = await windowManager.isMinimized();
    if (isMinimized) {
      await windowManager.restore();
    }

    // Check if window is maximized
    final isMaximized = await windowManager.isMaximized();
    if (isMaximized) {
      await windowManager.unmaximize();
    }
  }
}

2. Platform-Specific Features

class PlatformFeaturesExample extends StatelessWidget {
  Future<void> _handlePlatformFeatures() async {
    if (Platform.isWindows) {
      // Windows-specific features
      await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
    } else if (Platform.isMacOS) {
      // macOS-specific features
      await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
    } else if (Platform.isLinux) {
      // Linux-specific features
      await windowManager.setTitleBarStyle(TitleBarStyle.normal);
    }
  }
}

Conclusion

Building desktop applications with Flutter involves:

  • Setting up the development environment
  • Implementing platform-specific features
  • Managing windows and system integration
  • Optimizing performance
  • Handling platform-specific considerations

Remember to:

  • Test on all target platforms
  • Implement proper window management
  • Consider platform-specific UI/UX
  • Optimize for performance
  • Handle platform-specific features appropriately

With these techniques, you can create powerful and native-feeling desktop applications using Flutter!