Flutter 与 OpenHarmony 深度融合:实现分布式文件共享与跨设备协同编辑系统

引言

在多设备协同办公场景中,用户常面临这样的痛点:

  • 手机上收到一份合同,想用平板的大屏签字;
  • 在 PC 上写了一半的文档,通勤路上想用手机继续编辑;
  • 家人用电视查看照片,你希望实时添加新拍的照片到相册。

OpenHarmony 提供了强大的 分布式文件服务(Distributed File Service, DFS) ,支持跨设备文件自动同步、共享访问、协同编辑。而 Flutter 凭借其高性能 UI 能力,可构建统一的文档/媒体管理界面。

本文将带你从零开发一个 "分布式协作文档中心",实现:

  • 多设备间 Markdown 文档自动同步
  • 支持 多人同时编辑(OT 算法基础版);
  • 文件通过 分布式 URI 安全共享;
  • 编辑内容实时预览(Flutter + Markdown 渲染)。

这是目前社区首篇完整实现 Flutter + OpenHarmony 分布式文件协同的实战教程


一、技术原理:DFS 如何工作?

OpenHarmony 的分布式文件系统基于 分布式数据管理(DDM) + 软总线(DSoftBus),核心特性包括:

  • 统一命名空间dfs://<bundleId>/<path> 可跨设备访问;

  • 自动同步:文件变更后,系统自动推送到可信设备;

  • 权限控制:仅同应用、同账号、已配对设备可访问;

  • 断点续传:大文件传输支持中断恢复。

    +------------------+ +------------------+
    | 手机 (Flutter) | | 平板 (Flutter) |
    | - 创建 doc.md |<----->| - 实时看到更新 |
    +--------+---------+ DFS +--------+---------+
    | |
    [DistributedFileManager] [DistributedFileManager]
    | |
    +---------- 共享文件 <--------+
    dfs://com.example.docs/docs/doc.md

✅ 优势:开发者无需手动处理网络传输、冲突合并、权限校验。


二、整体架构设计

MethodChannel DSoftBus Flutter UI DfsFilePlugin DistributedFileManager 本地文件系统 远程设备 DFS Markdown 预览 协同编辑状态

关键模块:

  • DfsFilePlugin:封装 DFS API,提供 Dart 接口;
  • DistributedFileManager:OpenHarmony 原生文件管理器;
  • 协同编辑引擎:基于简易 OT(Operational Transformation)算法;
  • 实时预览 :使用 flutter_markdown 渲染。

三、原生侧:分布式文件操作封装(ArkTS)

1. 权限与配置

json 复制代码
// module.json5
{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.DISTRIBUTED_DATASYNC" },
      { "name": "ohos.permission.READ_MEDIA" },
      { "name": "ohos.permission.WRITE_MEDIA" }
    ]
  }
}

2. 创建 DfsFileManager.ets

ts 复制代码
// services/DfsFileManager.ets
import fileManager from '@ohos.file.distributedFileManager';
import fs from '@ohos.file.fs';

type FileInfo = {
  uri: string;
  name: string;
  size: number;
  lastModified: number;
};

class DfsFileManager {
  private bundleName: string;

  constructor(bundleName: string) {
    this.bundleName = bundleName;
  }

  // 获取分布式根目录 URI
  getDfsRootUri(): string {
    return `dfs://${this.bundleName}/docs/`;
  }

  // 列出所有文档
  async listFiles(): Promise<FileInfo[]> {
    const rootUri = this.getDfsRootUri();
    try {
      const files = await fileManager.listFiles(rootUri);
      const result: FileInfo[] = [];
      for (const file of files) {
        const stat = await fileManager.stat(file.uri);
        result.push({
          uri: file.uri,
          name: file.name,
          size: stat.size,
          lastModified: stat.mtime.getTime()
        });
      }
      return result;
    } catch (err) {
      console.error('[DFS] listFiles failed:', err);
      return [];
    }
  }

  // 读取文件内容(UTF-8)
  async readFile(uri: string): Promise<string> {
    const fd = await fileManager.openFile(uri, fs.OpenMode.READ_ONLY);
    const buffer = new ArrayBuffer(1024 * 1024); // 1MB max
    const bytesRead = await fileManager.read(fd, buffer);
    await fileManager.close(fd);

    const uint8Array = new Uint8Array(buffer, 0, bytesRead);
    return String.fromCharCode(...uint8Array);
  }

  // 写入文件(覆盖)
  async writeFile(uri: string, content: string): Promise<boolean> {
    try {
      const fd = await fileManager.openFile(uri, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNCATE);
      const encoder = new TextEncoder();
      const buffer = encoder.encode(content);
      await fileManager.write(fd, buffer.buffer);
      await fileManager.close(fd);
      return true;
    } catch (err) {
      console.error('[DFS] writeFile failed:', err);
      return false;
    }
  }

  // 创建新文件
  async createFile(name: string): Promise<string> {
    const uri = `${this.getDfsRootUri()}${name}`;
    await this.writeFile(uri, '# 新文档\n\n开始编辑...');
    return uri;
  }

  // 监听文件变更(用于协同)
  watchFile(uri: string, callback: (newContent: string) => void): void {
    fileManager.on('change', uri, () => {
      this.readFile(uri).then(content => callback(content));
    });
  }

  // 停止监听
  unwatchFile(uri: string): void {
    fileManager.off('change', uri);
  }
}

const dfsManager = new DfsFileManager('com.example.flutter.dfsdemo');
export default dfsManager;

3. 暴露给 Flutter(插件层)

ts 复制代码
// plugins/DfsFilePlugin.ets
import dfsManager from '../services/DfsFileManager';
import { MethodChannel, EventChannel } from '@flutter/engine';

const METHOD_CHANNEL = 'com.example.flutter/dfs/method';
const EVENT_CHANNEL = 'com.example.flutter/dfs/event';

export class DfsFilePlugin {
  private eventSink: any = null;
  private watchingUri: string | null = null;

  init() {
    const methodChannel = new MethodChannel(METHOD_CHANNEL);
    methodChannel.setMethodCallHandler(this.handleMethod.bind(this));

    const eventChannel = new EventChannel(EVENT_CHANNEL);
    eventChannel.setStreamHandler({
      onListen: (_, sink) => this.eventSink = sink,
      onCancel: () => this.eventSink = null
    });
  }

  private async handleMethod(call: any): Promise<any> {
    switch (call.method) {
      case 'listFiles':
        const files = await dfsManager.listFiles();
        return { files };

      case 'readFile':
        const content = await dfsManager.readFile(call.arguments['uri']);
        return { content };

      case 'writeFile':
        const success = await dfsManager.writeFile(
          call.arguments['uri'],
          call.arguments['content']
        );
        return { success };

      case 'createFile':
        const newUri = await dfsManager.createFile(call.arguments['name']);
        return { uri: newUri };

      case 'watchFile':
        if (this.watchingUri) {
          dfsManager.unwatchFile(this.watchingUri);
        }
        this.watchingUri = call.arguments['uri'];
        dfsManager.watchFile(this.watchingUri, (content) => {
          if (this.eventSink) {
            this.eventSink.success({ type: 'file_changed', content });
          }
        });
        return { success: true };

      case 'unwatchFile':
        if (this.watchingUri) {
          dfsManager.unwatchFile(this.watchingUri);
          this.watchingUri = null;
        }
        return { success: true };
    }
    throw new Error('Unknown method');
  }
}

EntryAbility.ets 中初始化:

ts 复制代码
new DfsFilePlugin().init();

四、Flutter 侧:协同编辑与预览

1. 封装服务

dart 复制代码
// lib/services/dfs_service.dart
import 'package:flutter/services.dart';

class DfsService {
  static const _method = MethodChannel('com.example.flutter/dfs/method');
  static const _event = EventChannel('com.example.flutter/dfs/event');

  static Future<List<Map<String, dynamic>>> listFiles() async {
    final result = await _method.invokeMethod('listFiles');
    return List<Map<String, dynamic>>.from(result['files']);
  }

  static Future<String> readFile(String uri) async {
    final result = await _method.invokeMethod('readFile', {'uri': uri});
    return result['content'] as String;
  }

  static Future<bool> writeFile(String uri, String content) async {
    final result = await _method.invokeMethod('writeFile', {
      'uri': uri,
      'content': content,
    });
    return result['success'] == true;
  }

  static Future<String> createFile(String name) async {
    final result = await _method.invokeMethod('createFile', {'name': name});
    return result['uri'] as String;
  }

  static Future<void> watchFile(String uri) async {
    await _method.invokeMethod('watchFile', {'uri': uri});
  }

  static Future<void> unwatchFile() async {
    await _method.invokeMethod('unwatchFile');
  }

  static Stream<Map<String, dynamic>> onEvent() async* {
    await for (final event in _event.receiveBroadcastStream()) {
      yield event as Map<String, dynamic>;
    }
  }
}

2. 协同编辑状态管理(简易 OT)

dart 复制代码
// lib/providers/editor_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final editorProvider = StateNotifierProvider<EditorManager, EditorState>((ref) {
  return EditorManager();
});

class EditorState {
  final String? currentFileUri;
  final String content;
  final bool isRemoteUpdating;

  EditorState({
    this.currentFileUri,
    this.content = '',
    this.isRemoteUpdating = false,
  });

  EditorState copyWith({
    String? currentFileUri,
    String? content,
    bool? isRemoteUpdating,
  }) {
    return EditorState(
      currentFileUri: currentFileUri ?? this.currentFileUri,
      content: content ?? this.content,
      isRemoteUpdating: isRemoteUpdating ?? this.isRemoteUpdating,
    );
  }
}

class EditorManager extends StateNotifier<EditorState> {
  EditorManager() : super(EditorState());

  Future<void> openFile(String uri) async {
    state = state.copyWith(currentFileUri: uri, isRemoteUpdating: true);
    final content = await DfsService.readFile(uri);
    await DfsService.watchFile(uri);
    state = state.copyWith(content: content, isRemoteUpdating: false);
  }

  Future<void> createNewFile(String name) async {
    final uri = await DfsService.createFile(name);
    await openFile(uri);
  }

  Future<void> updateContent(String newContent) async {
    if (state.isRemoteUpdating) return; // 防止本地覆盖远程变更
    if (state.currentFileUri != null) {
      await DfsService.writeFile(state.currentFileUri!, newContent);
      state = state.copyWith(content: newContent);
    }
  }

  void handleRemoteUpdate(String newContent) {
    state = state.copyWith(content: newContent, isRemoteUpdating: true);
    Future.delayed(Duration(milliseconds: 300), () {
      state = state.copyWith(isRemoteUpdating: false);
    });
  }

  @override
  void dispose() {
    DfsService.unwatchFile();
    super.dispose();
  }
}

3. 构建编辑界面

dart 复制代码
// lib/screens/editor_screen.dart
import 'package:flutter_markdown/flutter_markdown.dart';

class EditorScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(editorProvider);
    final editor = ref.read(editorProvider.notifier);

    // 监听远程变更
    useEffect(() {
      final sub = DfsService.onEvent().listen((event) {
        if (event['type'] == 'file_changed') {
          editor.handleRemoteUpdate(event['content'] as String);
        }
      });
      return sub.cancel;
    }, []);

    return Scaffold(
      appBar: AppBar(title: Text('协作文档')),
      body: Row(
        children: [
          // 左侧:编辑区
          Expanded(
            flex: 1,
            child: TextField(
              controller: TextEditingController(text: state.content),
              onChanged: (text) => editor.updateContent(text),
              maxLines: null,
              decoration: InputDecoration(
                hintText: '输入 Markdown...',
                border: InputBorder.none,
                contentPadding: EdgeInsets.all(16),
              ),
              enabled: !state.isRemoteUpdating,
            ),
          ),
          // 右侧:预览区
          Expanded(
            flex: 1,
            child: Container(
              padding: EdgeInsets.all(16),
              child: Markdown(data: state.content),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showCreateDialog(context, editor),
        child: Icon(Icons.add),
      ),
    );
  }

  void _showCreateDialog(BuildContext context, EditorManager editor) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (_) => AlertDialog(
        title: Text('新建文档'),
        content: TextField(
          controller: controller,
          decoration: InputDecoration(hintText: '文件名.md'),
        ),
        actions: [
          TextButton(onPressed: Navigator.of(context).pop, child: Text('取消')),
          TextButton(
            onPressed: () {
              final name = controller.text.trim();
              if (name.isNotEmpty) {
                editor.createNewFile(name.endsWith('.md') ? name : '$name.md');
                Navigator.of(context).pop();
              }
            },
            child: Text('创建'),
          )
        ],
      ),
    );
  }
}

五、关键问题与解决方案

问题 解决方案
多人同时编辑冲突 使用 OT 算法(本文简化为"最后写入胜出",生产环境需完整 OT/CRDT)
大文件卡顿 限制单文件大小(如 ≤1MB),或分块加载
文件列表不同步 启动时强制刷新,或监听 fileManager.on('dir_change')
URI 安全性 DFS URI 仅限同应用访问,无需额外加密

六、测试流程

  1. 手机和平板安装同一应用;
  2. 手机创建 report.md,输入内容;
  3. 平板自动出现该文件,打开后实时同步;
  4. 平板编辑内容,手机立即更新预览;
  5. 断开网络后各自编辑,重连后以最后修改时间为准合并(简化逻辑)。

七、总结

本文实现了 Flutter 应用通过 OpenHarmony DFS 进行分布式文件协同的完整方案,涵盖:

  • 文件自动同步:利用 DFS 统一命名空间;
  • 实时协同编辑:结合事件监听与状态管理;
  • 所见即所得:Markdown 实时渲染;
  • 安全共享:系统级权限保障。

此架构可轻松扩展至:

  • 照片/视频共享相册
  • 跨设备笔记同步
  • 团队项目文档协作

未来的办公,不再有"我的文件"和"你的文件",只有"我们的文件"。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

相关推荐
帅气马战的账号14 小时前
OpenHarmony与Flutter深度融合:分布式跨端开发全栈实践指南
flutter
敲上瘾4 小时前
MySQL主从集群解析:从原理到Docker实战部署
android·数据库·分布式·mysql·docker·数据库架构
遝靑4 小时前
Flutter 状态管理深度剖析:Provider/Bloc/GetX 原理 + 实战 + 选型(附避坑 & 性能对比)
flutter
豫狮恒4 小时前
OpenHarmony Flutter 分布式数据持久化:跨设备数据一致性与同步方案
分布式·安全·flutter·wpf·openharmony
ITKEY_4 小时前
flutter 运行windows版本报错
windows·flutter
SoleMotive.4 小时前
kafka和其他消息队列的区别
分布式·kafka
狮恒4 小时前
OpenHarmony Flutter 分布式能力调度:跨设备服务协同与资源共享方案
分布式·flutter·wpf·openharmony
小毅&Nora4 小时前
【后端】【诡秘架构】 ① 序列9:占卜家——分布式链路追踪入门:用 SkyWalking 预知系统命运
分布式·架构·skywalking
He BianGu4 小时前
【笔记】在WPF App.cs中结合 IServiceCollection 进行 IOC 依赖注入
数据库·笔记·wpf