6.6 Flutter 与桌面应用 (Desktop Applications)

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

plaintext
复制代码

**4. 桌面平台特有功能**

*   **窗口管理**:调整窗口大小、位置、全屏等。
*   **菜单栏**:创建和管理应用菜单。
*   **文件操作**:读写文件、选择文件/目录。
*   **通知**:发送桌面通知。
*   **系统托盘**:将应用最小化到系统托盘。
*   **键盘快捷键**:自定义键盘快捷键。

这些功能通常通过 Flutter 插件来实现,例如 `window_manager`、`menubar`、`file_selector`、`desktop_notifications` 等。

#### 官方文档链接

*   [Flutter 桌面开发](https://docs.flutter.cn/platform-integration/desktop)
*   [Flutter 桌面应用示例](https://github.com/flutter/samples/tree/main/desktop_gallery)

#### Flutter 开发中的应用案例

Flutter 桌面应用适用于各种场景,如生产力工具、数据可视化、本地文件管理、企业内部应用等。以下案例将演示如何构建一个简单的桌面文件浏览器,展示 Flutter 如何与文件系统交互。

**案例:构建一个简单的桌面文件浏览器**

我们将创建一个 Flutter 桌面应用,允许用户选择一个目录,然后显示该目录下的文件和子目录。

**步骤 1: 创建项目并添加依赖**

```bash
flutter create desktop_file_browser --platforms=windows,macos,linux
cd desktop_file_browser

pubspec.yaml 中添加 file_selector 依赖:

yaml
复制代码
dependencies:
  flutter:
    sdk: flutter
  file_selector: ^1.0.0 # 最新版本可能不同

运行 flutter pub get

步骤 2: 构建 UI 和逻辑 (lib/main.dart)

dart
复制代码
import 'package:flutter/material.dart';
import 'package:file_selector/file_selector.dart';
import 'dart:io';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '桌面文件浏览器',
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
      ),
      home: const FileBrowserScreen(),
    );
  }
}

class FileBrowserScreen extends StatefulWidget {
  const FileBrowserScreen({super.key});

  @override
  State<FileBrowserScreen> createState() => _FileBrowserScreenState();
}

class _FileBrowserScreenState extends State<FileBrowserScreen> {
  String? _currentDirectoryPath;
  List<FileSystemEntity> _filesAndFolders = [];
  bool _isLoading = false;

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

  Future<void> _loadInitialDirectory() async {
    // 尝试加载用户文档目录作为初始目录
    try {
      final String? initialPath = await getDirectoryPath(); // 弹出目录选择器
      if (initialPath != null) {
        await _loadDirectoryContents(initialPath);
      }
    } catch (e) {
      print('Error loading initial directory: $e');
      setState(() {
        _currentDirectoryPath = '无法加载初始目录';
      });
    }
  }

  Future<void> _loadDirectoryContents(String path) async {
    setState(() {
      _isLoading = true;
      _currentDirectoryPath = path;
      _filesAndFolders = [];
    });

    try {
      final Directory directory = Directory(path);
      if (await directory.exists()) {
        final List<FileSystemEntity> entities = await directory.list().toList();
        entities.sort((a, b) {
          // 目录排在文件前面
          if (a is Directory && b is File) return -1;
          if (a is File && b is Directory) return 1;
          return a.path.toLowerCase().compareTo(b.path.toLowerCase());
        });
        setState(() {
          _filesAndFolders = entities;
        });
      } else {
        setState(() {
          _currentDirectoryPath = '目录不存在: $path';
        });
      }
    } catch (e) {
      setState(() {
        _currentDirectoryPath = '读取目录失败: $e';
      });
      print('Error reading directory: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _pickDirectory() async {
    final String? selectedDirectory = await getDirectoryPath();
    if (selectedDirectory != null) {
      await _loadDirectoryContents(selectedDirectory);
    }
  }

  void _openItem(FileSystemEntity entity) async {
    if (entity is Directory) {
      await _loadDirectoryContents(entity.path);
    } else if (entity is File) {
      // 模拟打开文件,实际应用中可能需要调用平台特定的API
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('尝试打开文件: ${entity.path}')),
      );
      // 在macOS/Linux上可以使用 `open` 命令,Windows上可以使用 `start` 命令
      // import 'package:process_run/process_run.dart';
      // await run('open', [entity.path]); // macOS/Linux
      // await run('start', [entity.path]); // Windows
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('桌面文件浏览器'),
        actions: [
          IconButton(
            icon: const Icon(Icons.folder_open),
            onPressed: _pickDirectory,
            tooltip: '选择目录',
          ),
          if (_currentDirectoryPath != null && _currentDirectoryPath != '无法加载初始目录' && _currentDirectoryPath != '读取目录失败: $e')
            IconButton(
              icon: const Icon(Icons.arrow_upward),
              onPressed: () {
                final parent = Directory(_currentDirectoryPath!).parent;
                if (parent.path != _currentDirectoryPath) {
                  _loadDirectoryContents(parent.path);
                }
              },
              tooltip: '返回上一级',
            ),
        ],
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              '当前目录: ${_currentDirectoryPath ?? '未选择'}',
              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
          ),
          Expanded(
            child: _isLoading
                ? const Center(child: CircularProgressIndicator())
                : _filesAndFolders.isEmpty && _currentDirectoryPath != null && _currentDirectoryPath != '无法加载初始目录' && _currentDirectoryPath != '读取目录失败: $e'
                    ? const Center(child: Text('此目录为空'))
                    : ListView.builder(
                        itemCount: _filesAndFolders.length,
                        itemBuilder: (context, index) {
                          final entity = _filesAndFolders[index];
                          final isDirectory = entity is Directory;
                          return ListTile(
                            leading: Icon(isDirectory ? Icons.folder : Icons.insert_drive_file),
                            title: Text(entity.path.split(Platform.pathSeparator).last),
                            onTap: () => _openItem(entity),
                          );
                        },
                      ),
          ),
        ],
      ),
    );
  }
}

案例分析:

  • file_selector 插件:这个插件提供了跨平台的 API 来选择文件和目录。getDirectoryPath() 方法会打开一个原生的目录选择对话框。
  • dart:io:Flutter 桌面应用可以直接使用 Dart 的 dart:io 库来访问文件系统,进行文件和目录的读写、遍历等操作。
  • DirectoryFiledart:io 提供了 DirectoryFile 类来表示文件系统中的目录和文件。
  • directory.list().toList():用于获取目录下的所有文件和子目录。
  • Platform.pathSeparator:用于获取当前操作系统的路径分隔符(例如 Windows 是 \,macOS/Linux 是 /)。
  • UI 交互
    • IconButton 用于触发目录选择和返回上一级目录的操作。
    • ListView.builder 用于高效地显示文件和目录列表。
    • ListTile 用于显示每个文件或目录的图标和名称。
    • onTap 回调用于处理用户点击事件,如果是目录则进入该目录,如果是文件则模拟打开。
  • 错误处理和加载状态:应用中包含了 _isLoading 状态和错误信息显示,以提供更好的用户体验。

如何运行和测试:

  1. 运行应用:在你的桌面操作系统上运行 flutter run -d windows (或 macos, linux)。
  2. 选择目录:应用启动后会弹出一个目录选择器,选择一个目录。
  3. 浏览文件:应用会显示所选目录下的文件和子目录。你可以点击子目录进入,点击“返回上一级”按钮返回。

这个案例展示了 Flutter 在桌面应用开发中的强大能力,特别是与本地文件系统的深度集成。通过结合 Flutter 的 UI 能力和 dart:io 库,你可以构建功能丰富的桌面应用程序。

总结

Flutter 桌面应用开发已经成熟,为开发者提供了一个高效、跨平台的解决方案。通过利用 Flutter 丰富的 Widget 库和强大的平台集成能力,你可以构建出高性能、美观且功能完善的桌面应用程序。