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. 权限与配置

// module.json5
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.DISTRIBUTED_DATASYNC" },
{ "name": "ohos.permission.READ_MEDIA" },
{ "name": "ohos.permission.WRITE_MEDIA" }
]
}
}

2. 创建 DfsFileManager.ets

// 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(插件层)

// 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 中初始化:

new DfsFilePlugin().init();

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

1. 封装服务

// 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)

// 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);
          });
          }
          
          void dispose() {
          DfsService.unwatchFile();
          super.dispose();
          }
          }

3. 构建编辑界面

// lib/screens/editor_screen.dart
import 'package:flutter_markdown/flutter_markdown.dart';
class EditorScreen extends ConsumerWidget {

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 实时渲染;
  • 安全共享:系统级权限保障。

此架构可轻松扩展至:

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

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

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

posted @ 2026-01-09 10:09  clnchanpin  阅读(12)  评论(0)    收藏  举报