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 — отличный выбор для кроссплатформенных десктоп приложений. Учитывайте особенности каждой платформы при разработке.