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

此架构可轻松扩展至:

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

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

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

相关推荐
火柴就是我3 小时前
学习一些常用的混合模式之BlendMode. dst_atop
android·flutter
火柴就是我4 小时前
学习一些常用的混合模式之BlendMode. dstIn
android·flutter
巧克力味的桃子6 小时前
Spark 课程核心知识点复习汇总
大数据·分布式·spark
火柴就是我6 小时前
学习一些常用的混合模式之BlendMode. dst
android·flutter
Java 码农6 小时前
RabbitMQ集群部署方案及配置指南05
分布式·rabbitmq
前端不太难6 小时前
Sliver 为什么能天然缩小 rebuild 影响面
flutter·性能优化·状态模式
小马爱打代码7 小时前
ZooKeeper:五种经典应用场景
分布式·zookeeper·云原生
带带弟弟学爬虫__8 小时前
Flutter 逆向想学却无从下手?
flutter
行者968 小时前
Flutter跨平台开发:颜色选择器适配OpenHarmony
flutter·harmonyos·鸿蒙
不爱吃糖的程序媛8 小时前
深度解析OpenHarmony跨平台框架生态:RN、Flutter、Cordova、KMP四大方向全梳理
flutter