1. 项目背景

在桌面应用开发中,多窗口协同工作是一个常见需求。特别是在需要同时展示不同内容或进行多任务操作时,主副屏架构能够显著提升用户体验。本文基于一个Flutter桌面应用项目,详细介绍如何实现主副屏之间的实时数据同步。

2. 技术架构

2.1 核心依赖

- Flutter:跨平台UI框架
- desktop_multi_window:Flutter桌面多窗口支持库
- Dart Streams:用于事件流管理

2.2 架构设计

本项目采用以下架构设计:

1. WindowManager:核心管理器,负责窗口生命周期和数据同步
2. 主窗口:控制中心,管理所有子窗口
3. 子窗口:功能窗口,接收主窗口的数据更新

3. 核心功能实现

3.1 窗口管理

WindowManager类负责管理所有窗口的创建、销毁和通信:

class WindowManager {
  // 窗口存储
  final Map<int, WindowInfo> _windows = {};
  
  // 创建新窗口
  Future<WindowInfo> createWindow({
    required String name,
    required Map<String, dynamic> args,
    Size size = const Size(800, 600),
    Offset? position,
    String? title,
    bool showImmediately = true,
  }) async {
    // 生成窗口ID
    final windowId = _generateWindowId();
    
    // 准备窗口参数
    final windowArgs = {
      'windowId': windowId,
      'windowName': name,
      'windowArgs': args,
      'parentWindowId': 0,
      'createdAt': DateTime.now().millisecondsSinceEpoch,
      'type': 'sub_window',
    };
    
    // 创建窗口控制器
    final controller = await DesktopMultiWindow.createWindow(
      jsonEncode(windowArgs),
    );
    
    // 配置窗口
    controller
      ..setFrame((position ?? const Offset(100, 100)) & size)
      ..center()
      ..setTitle(title ?? name);
    
    // 立即显示
    if (showImmediately) {
      await controller.show();
    }
    
    // 创建窗口信息
    final windowInfo = WindowInfo(
      id: windowId,
      controller: controller,
      name: name,
      title: title,
      isVisible: showImmediately,
    );
    
    // 保存窗口
    _windows[windowId] = windowInfo;
    
    // 发送创建事件
    _eventController.add(WindowEvent(
      type: WindowEventType.windowCreated,
      windowId: windowId,
      data: {
        'name': name,
        'title': title,
        'size': {'width': size.width, 'height': size.height},
        'position': {'x': position?.dx ?? 100, 'y': position?.dy ?? 100},
      },
    ));
    
    return windowInfo;
  }
}

3.2 数据管理与同步

WindowManager类还负责管理共享数据和数据订阅:

class WindowManager {
  // 数据管理 - 用于实时数据同步
  final Map<String, dynamic> _sharedData = {};
  final Map<String, List<int>> _dataSubscriptions = {}; // 数据键 -> 订阅窗口ID列表
  
  // 设置共享数据
  Future<void> setData(String key, dynamic value) async {
    _sharedData[key] = value;
    await _broadcastDataUpdate(key, value);
  }
  
  // 批量设置共享数据
  Future<void> setMultipleData(Map<String, dynamic> data) async {
    for (final entry in data.entries) {
      _sharedData[entry.key] = entry.value;
      await _broadcastDataUpdate(entry.key, entry.value);
    }
  }
  
  // 订阅数据
  Future<void> subscribeData(int windowId, List<String> keys) async {
    for (final key in keys) {
      if (!_dataSubscriptions.containsKey(key)) {
        _dataSubscriptions[key] = [];
      }
      if (!_dataSubscriptions[key]!.contains(windowId)) {
        _dataSubscriptions[key]!.add(windowId);
        debugPrint('窗口 $windowId 订阅了数据: $key');
      }
    }
  }
  
  // 广播数据更新
  Future<void> _broadcastDataUpdate(String key, dynamic value) async {
    if (_dataSubscriptions.containsKey(key)) {
      final subscribers = _dataSubscriptions[key]!;
      for (final windowId in subscribers) {
        if (_windows.containsKey(windowId)) {
          try {
            await sendMessage(windowId, 'data_updated', {
              'key': key,
              'value': value,
              'timestamp': DateTime.now().millisecondsSinceEpoch,
            });
          } catch (e) {
            debugPrint('广播数据更新到窗口 $windowId 失败: $e');
          }
        }
      }
    }
  }
}

3.3 子窗口数据更新处理

子窗口需要处理来自主窗口的数据更新消息:
 

class SubWindowPage extends StatefulWidget {
  final Map<String, dynamic> windowArgs;

  const SubWindowPage({super.key, required this.windowArgs});

  @override
  State<SubWindowPage> createState() => _SubWindowPageState();
}

class _SubWindowPageState extends State<SubWindowPage> {
  late int _windowId;
  
  @override
  void initState() {
    super.initState();
    _windowId = widget.windowArgs['windowId'] ?? 0;
    _setupMessageHandler();
  }
  
  void _setupMessageHandler() {
    DesktopMultiWindow.setMethodHandler((call, fromWindowId) async {
      // 处理数据更新消息
      if (call.method == 'data_updated') {
        Map<String, dynamic> data = {};
        if (call.arguments != null) {
          if (call.arguments is Map) {
            data = Map<String, dynamic>.fromEntries(
              (call.arguments as Map).entries.map((e) => MapEntry(e.key.toString(), e.value)),
            );
          }
        }
        
        final key = data['key']?.toString();
        final value = data['value'];
        
        print('窗口 $_windowId 收到数据更新: $key = $value');
        
        // 显示通知
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('数据更新: $key = $value'),
              duration: const Duration(seconds: 2),
            ),
          );
        }
        
        // 触发UI更新
        setState(() {});
        
        return {'status': 'received', 'key': key};
      }
      
      // 其他消息处理...
    });
  }
}

3.4 主窗口数据管理界面

主窗口提供数据管理界面,用于设置共享数据和管理订阅:
 

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

  @override
  State<MainWindow> createState() => _MainWindowState();
}

class _MainWindowState extends State<MainWindow> {
  late WindowManager _windowManager;
  
  // 数据管理相关
  final TextEditingController _dataKeyController = TextEditingController(text: 'counter');
  final TextEditingController _dataValueController = TextEditingController(text: '0');
  int _counter = 0;
  
  @override
  void initState() {
    super.initState();
    _windowManager = WindowManager();
    _setupEventListeners();
  }
  
  // 更新数据
  Future<void> _updateData() async {
    final key = _dataKeyController.text.trim();
    final value = _dataValueController.text.trim();
    
    if (key.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('数据键不能为空')),
      );
      return;
    }
    
    try {
      // 尝试将值转换为适当的类型
      dynamic parsedValue = value;
      if (int.tryParse(value) != null) {
        parsedValue = int.parse(value);
      } else if (double.tryParse(value) != null) {
        parsedValue = double.parse(value);
      } else if (value.toLowerCase() == 'true') {
        parsedValue = true;
      } else if (value.toLowerCase() == 'false') {
        parsedValue = false;
      }
      
      await _windowManager.setData(key, parsedValue);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('数据已更新: $key = $parsedValue'),
          backgroundColor: Colors.green,
        ),
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('更新数据失败: $e'),
          backgroundColor: Colors.red,
        ),
      );
    }
  }
  
  // 订阅所有窗口
  Future<void> _subscribeAllWindows() async {
    final windows = _windowManager.getVisibleWindows();
    if (windows.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('没有活动窗口')),
      );
      return;
    }
    
    final key = _dataKeyController.text.trim();
    if (key.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('数据键不能为空')),
      );
      return;
    }
    
    for (final window in windows) {
      await _windowManager.subscribeData(window.id, [key]);
    }
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('所有窗口已订阅数据: $key'),
        backgroundColor: Colors.green,
      ),
    );
  }
}

4. 遇到的问题与解决方案

4.1 类型转换错误

PlatformException(error, type '_Map<Object?, Object?>' is not a subtype of type 'Map<String, dynamic>?' in type cast, null, null)

原因:
Flutter平台通道在传递数据时,类型信息会丢失,导致返回 `_Map<Object?, Object?>` 类型而不是 `Map<String, dynamic>` 类型。

解决方案:
使用安全的类型转换方法:

Map<String, dynamic> data = {};
if (call.arguments != null) {
  if (call.arguments is Map) {
    data = Map<String, dynamic>.fromEntries(
      (call.arguments as Map).entries.map((e) => MapEntry(e.key.toString(), e.value)),
    );
  }
}

4.2 数据订阅失败

问题:
子窗口没有收到主窗口的数据更新通知。

原因:
`subscribeData` 方法实现错误,试图向子窗口发送 `subscribe_data` 消息,而子窗口没有处理该消息的逻辑。

解决方案:
修改 `subscribeData` 方法,直接在本地管理订阅关系:

Future<void> subscribeData(int windowId, List<String> keys) async {
  for (final key in keys) {
    if (!_dataSubscriptions.containsKey(key)) {
      _dataSubscriptions[key] = [];
    }
    if (!_dataSubscriptions[key]!.contains(windowId)) {
      _dataSubscriptions[key]!.add(windowId);
      debugPrint('窗口 $windowId 订阅了数据: $key');
    }
  }
}

4.3 窗口未找到错误

问题:

PlatformException(-1, target window not found., null, null)

原因:
当窗口已销毁但仍在 `_windows` 映射中时,尝试向该窗口发送消息会失败。

解决方案:
在 `sendMessage` 方法中添加错误处理,自动清理已销毁的窗口:

Future<dynamic> sendMessage(
  int targetWindowId,
  String method,
  Map<String, dynamic> arguments,
) async {
  if (!_windows.containsKey(targetWindowId)) {
    throw Exception('目标窗口 $targetWindowId 不存在');
  }

  try {
    final dynamic result = await DesktopMultiWindow.invokeMethod(
      targetWindowId,
      method,
      arguments,
    );

    // 更新窗口活动时间
    if (_windows.containsKey(targetWindowId)) {
      _windows[targetWindowId]!.updateLastUpdated();
    }

    return result;
  } catch (e) {
    // 如果窗口不存在或已销毁,从映射中移除
    if (e is PlatformException && e.code == '-1') {
      _windows.remove(targetWindowId);
      debugPrint('窗口 $targetWindowId 已不存在,从映射中移除');
    }
    rethrow;
  }
}

5. 最佳实践

5.1 代码组织

1. 模块化设计:将窗口管理、数据同步等功能分离到不同模块
2. 错误处理:所有跨窗口调用都应包含错误处理
3. 状态管理:使用合适的状态管理方案,如 Provider 或 Bloc

5.2 性能优化

1. 批量更新:使用 `setMultipleData` 批量更新数据,减少广播次数
2. 选择性订阅:只订阅需要的数据,避免不必要的更新
3. 防抖处理:对于频繁更新的数据,考虑添加防抖机制

5.3 安全性

1. 数据验证:验证所有输入数据,避免注入攻击
2. 权限控制:实现窗口权限系统,控制数据访问权限
3. 错误日志:记录所有错误,便于调试和问题排查

6. 测试方法

6.1 基本测试

1. 启动应用:运行Flutter应用,打开主窗口
2. 创建子窗口:点击 "创建子窗口" 按钮,创建一个或多个子窗口
3. 配置数据订阅:
   - 在 "数据键" 输入框中输入 `counter`
   - 在 "数据值" 输入框中输入 `0`
   - 点击 "订阅所有窗口" 按钮
4. 测试数据同步**:
   - 点击 "增加计数器" 按钮
   - 观察子窗口是否显示 "数据更新: counter = X" 的通知
   - 重复点击,验证实时性

6.2 边界测试

1. **窗口销毁测试**:销毁一个子窗口后,验证其他窗口仍能正常接收数据更新
2. **网络延迟测试**:模拟网络延迟,验证系统的稳定性
3. **大量数据测试**:传输大量数据,验证系统的性能表现

7. 总结与未来展望

7.1 项目成果

本项目成功实现了以下功能:

1. 多窗口管理:支持创建、销毁和管理多个窗口
2. 实时数据同步:主窗口数据更新时,子窗口实时响应
3. 错误处理:完善的错误处理机制,确保系统稳定性
4. 用户友好:直观的用户界面,便于操作和测试

7.2 技术价值

本方案的技术价值在于:

1. 跨平台兼容:基于Flutter,支持Windows、macOS和Linux
2. 可扩展性强:模块化设计,易于添加新功能
3. 性能优异:实时数据同步,响应迅速
4. 代码质量高:遵循Flutter最佳实践,代码清晰可维护

7.3 未来展望

未来可以从以下方面进一步完善:

1. 增加安全性:实现更完善的权限控制和数据加密
2. 优化性能:添加数据缓存和批量处理机制
3. 增强功能:支持窗口布局保存和恢复
4. 跨平台统一:确保在不同平台上的表现一致
5. 文档完善:编写更详细的API文档和使用指南

8. 结语

Flutter桌面多窗口功能为桌面应用开发提供了新的可能性。通过本文介绍的主副屏实时数据同步方案,开发者可以快速实现复杂的多窗口协同应用,提升用户体验和工作效率。

希望本文对您的Flutter桌面应用开发有所帮助,如有任何问题或建议,欢迎在评论区交流讨论!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐