引言
在多设备协同办公场景中,用户常面临这样的痛点:
- 手机上收到一份合同,想用平板的大屏签字;
- 在 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 仅限同应用访问,无需额外加密 |
六、测试流程
- 手机和平板安装同一应用;
- 手机创建
report.md,输入内容; - 平板自动出现该文件,打开后实时同步;
- 平板编辑内容,手机立即更新预览;
- 断开网络后各自编辑,重连后以最后修改时间为准合并(简化逻辑)。
七、总结
本文实现了 Flutter 应用通过 OpenHarmony DFS 进行分布式文件协同的完整方案,涵盖:
- 文件自动同步:利用 DFS 统一命名空间;
- 实时协同编辑:结合事件监听与状态管理;
- 所见即所得:Markdown 实时渲染;
- 安全共享:系统级权限保障。
此架构可轻松扩展至:
- 照片/视频共享相册;
- 跨设备笔记同步;
- 团队项目文档协作。
未来的办公,不再有"我的文件"和"你的文件",只有"我们的文件"。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。