Рубрики
Flutter

Flutter для Desktop: Полное руководство 2025

Полное руководство по Flutter Desktop: Windows, macOS, Linux, особенности, menu bar, tray, file system.

Flutter поддерживает десктоп платформы: Windows, macOS и Linux. Разберём особенности разработки десктоп приложений.

Поддерживаемые платформы

Windows

flutter create --platforms windows .
flutter run -d windows

macOS

flutter create --platforms macos .
flutter run -d macos

Linux

flutter create --platforms linux .
flutter run -d linux

Требования

Windows

  • Windows 10 или выше
  • Visual Studio 2022 с C++ workload
  • Windows 10 SDK

macOS

  • macOS 10.14 или выше
  • Xcode 13 или выше
  • CocoaPods

Linux

  • Ubuntu 18.04 или выше (или эквивалент)
  • clang, cmake, ninja-build, pkg-config
  • GTK development headers

Desktop специфичные виджеты

MenuBar

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PlatformMenuBar(
      menus: [
        PlatformMenu(
          label: 'File',
          menus: [
            PlatformMenuItem(
              label: 'New',
              onSelected: () => _createNewFile(),
              shortcut: const SingleActivator(
                LogicalKeyboardKey.keyN,
                control: true,
              ),
            ),
            PlatformMenuItem(
              label: 'Open',
              onSelected: () => _openFile(),
              shortcut: const SingleActivator(
                LogicalKeyboardKey.keyO,
                control: true,
              ),
            ),
            PlatformMenuDivider(),
            PlatformMenuItemGroup(
              members: [
                PlatformMenuItem(
                  label: 'Recent',
                  isEnabled: false,
                ),
              ],
            ),
          ],
        ),
        PlatformMenu(
          label: 'Edit',
          menus: [
            PlatformMenuItem(
              label: 'Undo',
              onSelected: () => _undo(),
            ),
            PlatformMenuItem(
              label: 'Redo',
              onSelected: () => _redo(),
            ),
          ],
        ),
      ],
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}

Window Controls

import 'package:bitsdojo_window/bitsdojo_window.dart';

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

  // Настройка окна
  doWhenWindowReady(() {
    final win = appWindow;

    const initialSize = Size(800, 600);
    win.minSize = initialSize;
    win.size = initialSize;
    win.alignment = Alignment.center;
    win.title = 'My Flutter App';
    win.show();
  });
}

// Кастомный title bar
class CustomTitleBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WindowTitleBarBox(
      child: Row(
        children: [
          Expanded(child: MoveWindow()),
          const WindowButtons(),
        ],
      ),
    );
  }
}

Tray Icon

dependencies:
  system_tray: ^2.0.0
import 'package:system_tray/system_tray.dart';

class TrayManager {
  final SystemTray _systemTray = SystemTray();

  Future<void> initTray() async {
    await _systemTray.initSystemTray(
      iconPath: 'assets/tray_icon.png',
    );

    final menu = [
      MenuItem(label: 'Show', onClicked: _showWindow),
      MenuItem(label: 'Hide', onClicked: _hideWindow),
      MenuItem.separator(),
      MenuItem(label: 'Exit', onClicked: _exitApp),
    ];

    await _systemTray.setContextMenu(menu);
  }

  void _showWindow() {}
  void _hideWindow() {}
  void _exitApp() {}
}

File System

File Picker

dependencies:
  file_picker: ^8.0.0
import 'package:file_picker/file_picker.dart';

Future<void> pickFile() async {
  final result = await FilePicker.platform.pickFiles(
    type: FileType.custom,
    allowedExtensions: ['jpg', 'png', 'pdf'],
    allowMultiple: false,
  );

  if (result != null) {
    final file = result.files.single;

    print('File path: ${file.path}');
    print('File name: ${file.name}');
    print('File size: ${file.size}');
  }
}

Future<void> pickDirectory() async {
  final directory = await FilePicker.platform.getDirectoryPath();

  if (directory != null) {
    print('Selected directory: $directory');
  }
}

Сохранение файлов

Future<void> saveFile(String content) async {
  final outputPath = await FilePicker.platform.saveFile(
    dialogTitle: 'Save file',
    fileName: 'document.txt',
    type: FileType.custom,
    allowedExtensions: ['txt'],
  );

  if (outputPath != null) {
    final file = File(outputPath);
    await file.writeAsString(content);
  }
}

Drag and Drop

dependencies:
  desktop_drop: ^0.5.0
import 'package:desktop_drop/desktop_drop.dart';

class DropZone extends StatefulWidget {
  @override
  State<DropZone> createState() => _DropZoneState();
}

class _DropZoneState extends State<DropZone> {
  final List<String> _droppedFiles = [];

  @override
  Widget build(BuildContext context) {
    return DropTarget(
      onDragEntered: (details) {
        setState(() {});
      },
      onDragExited: (details) {
        setState(() {});
      },
      onDragDone: (details) {
        setState(() {
          _droppedFiles.addAll(
            details.files.map((e) => e.path),
          );
        });
      },
      child: Container(
        color: _droppedFiles.isEmpty ? Colors.grey[200] : Colors.green[200],
        child: Center(
          child: Column(
            children: [
              Text('Drop files here'),
              ..._droppedFiles.map((path) => Text(path)),
            ],
          ),
        ),
      ),
    );
  }
}

Горячие клавиши

RawKeyboardListener

class KeyboardHandler extends StatefulWidget {
  @override
  State<KeyboardHandler> createState() => _KeyboardHandlerState();
}

class _KeyboardHandlerState extends State<KeyboardHandler> {
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return RawKeyboardListener(
      focusNode: _focusNode,
      autofocus: true,
      onKey: (event) {
        if (event is RawKeyDownEvent) {
          // Ctrl+S для сохранения
          if (event.logicalKey == LogicalKeyboardKey.keyS &&
              event.data.isControlPressed) {
            _saveFile();
          }

          // Ctrl+N для нового файла
          if (event.logicalKey == LogicalKeyboardKey.keyN &&
              event.data.isControlPressed) {
            _newFile();
          }
        }
      },
      child: Scaffold(
        body: Center(child: Text('Press Ctrl+S to save')),
      ),
    );
  }
}

Shortcuts Actions

class ShortcutApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Shortcuts(
      shortcuts: <LogicalKeySet, Intent>{
        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
            const SaveIntent(),
        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyN):
            const NewIntent(),
      },
      child: Actions(
        actions: <Type, Action<Intent>>{
          SaveIntent: CallbackAction<SaveIntent>(
            onInvoke: (intent) => _saveFile(),
          ),
          NewIntent: CallbackAction<NewIntent>(
            onInvoke: (intent) => _newFile(),
          ),
        },
        child: Focus(
          autofocus: true,
          child: MaterialApp(
            home: HomeScreen(),
          ),
        ),
      ),
    );
  }
}

class SaveIntent extends Intent {
  const SaveIntent();
}

class NewIntent extends Intent {
  const NewIntent();
}

Window Manager

Размер окна

import 'dart:io';

class WindowManager {
  Future<void> setWindowSize(double width, double height) async {
    if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
      // Используйте bitsdojo_window или window_manager
    }
  }
}

Полноэкранный режим

dependencies:
  window_manager: ^0.3.0
import 'package:window_manager/window_manager.dart';

Future<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,
  );

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

  runApp(MyApp());
}

// Полноэкранный режим
Future<void> toggleFullScreen() async {
  final isFullScreen = await windowManager.isFullScreen();
  if (isFullScreen) {
    await windowManager.setFullScreen(false);
  } else {
    await windowManager.setFullScreen(true);
  }
}

Desktop особенности

Права доступа

# macos/Runner/DebugProfile.entitlements
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>

Auto-start

dependencies:
  launch_at_startup: ^0.3.0
import 'package:launch_at_startup/launch_at_startup.dart';

Future<void> setupAutoStart() async {
  await LaunchAtStartup.instance.enable();
}

Уведомления

dependencies:
  local_notifier: ^0.2.0
import 'package:local_notifier/local_notifier.dart';

Future<void> showNotification() async {
  final localNotification = LocalNotification(
    title: 'My App',
    body: 'Notification body',
  );

  await localNotification.show();
}

Заключение

Flutter Desktop — отличный выбор для кроссплатформенных десктоп приложений. Учитывайте особенности каждой платформы при разработке.