终极挑战:Flutter 应用在 OpenHarmony 上实现跨设备无缝流转(Continuation)与软总线协同

前言

在 OpenHarmony 的分布式能力矩阵中,除了分布式软总线(DSoftBus)分布式数据管理(DDM) ,还有一个高阶特性------跨设备任务无缝流转(Continuation)。它允许用户在一个设备上启动任务(如编辑文档、看视频),然后"接力"到另一个设备继续操作,体验如同在同一台设备上。

然而,Flutter 作为跨平台 UI 框架,并不原生支持 Continuation 机制 。本文将深入剖析 OpenHarmony Continuation 的工作原理,并手把手教你如何在 Flutter 应用中实现从手机到平板的"待办事项编辑"无缝流转,同时结合软总线完成状态同步与设备发现。

这是目前社区首篇完整实现 Flutter + OpenHarmony Continuation 的实战教程


一、什么是 Continuation?

Continuation 是 OpenHarmony 提供的一种跨设备任务迁移能力,其核心流程如下:

  1. 源设备 (如手机)调用 continueAbility()
  2. 系统通过软总线向目标设备(如平板)发送迁移请求;
  3. 目标设备收到请求后,启动相同 Ability(或指定 Ability);
  4. 源设备传递上下文数据(如当前编辑内容、页面状态);
  5. 目标设备恢复任务,用户无感知切换。

⚠️ 注意:Continuation 要求两端运行同一应用(BundleName 相同),且已建立信任关系。


二、整体架构设计

复制代码
+------------------+       +------------------+
|   手机 (Source)  |       |   平板 (Target)  |
|                  |       |                  |
|  Flutter App     |       |  Flutter App     |
|  - UI + State    |       |  - UI + State    |
+--------+---------+       +--------+---------+
         |                          |
   MethodChannel              MethodChannel
         |                          |
+--------v---------+       +--------v---------+
|  OHOS Native     |<----->|  OHOS Native     |
|  - Continuation  | DSoftBus | - Continuation  |
|  - KVStore Sync  |<------>| - KVStore Sync  |
+------------------+       +------------------+

关键点:

  • Continuation 由原生 Ability 触发,Flutter 无法直接调用;
  • 需通过 MethodChannel 通知原生层发起流转
  • 流转完成后,目标设备需通知 Flutter 恢复状态
  • 同时保留 KVStore 同步 作为兜底机制。

三、原生侧:实现 Continuation 能力

1. 配置 module.json5 支持 Continuation

json 复制代码
{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "continuable": true, // 关键:启用流转
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["action.system.home"]
          }
        ]
      }
    ],
    "requestPermissions": [
      { "name": "ohos.permission.CONTINUATION_AUTHORIZATION" },
      { "name": "ohos.permission.DISTRIBUTED_DATASYNC" }
    ]
  }
}

2. 实现 EntryAbility.ets

ts 复制代码
// ets/entryability/EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import continuationManager from '@ohos.continuationManager';
import type { Callback } from '@ohos.base';

export default class EntryAbility extends UIAbility {
  private static instance: EntryAbility;

  onCreate() {
    EntryAbility.instance = this;
    console.info('[Ability] onCreate');
  }

  // 【关键】注册 Continuation 回调
  onWindowStageCreate(windowStage) {
    continuationManager.setContinuationCallback(this.context, {
      onContinue: (wantParam) => {
        console.info('[Continuation] onContinue called');
        // 可在此决定是否允许流转(如检查设备类型)
        return true;
      },

      onSaveData: (callback: Callback<string>) => {
        console.info('[Continuation] onSaveData');
        // 通知 Flutter 获取当前状态并序列化
        SoftBusPlugin.getInstance().requestAppState((state: string) => {
          callback.resolve(state); // 返回给目标设备
        });
      },

      onRestoreData: (restoreData: string, callback: Callback<boolean>) => {
        console.info('[Continuation] onRestoreData:', restoreData);
        // 将恢复数据传递给 Flutter
        SoftBusPlugin.getInstance().restoreAppState(restoreData);
        callback.resolve(true);
      },

      onComplete: () => {
        console.info('[Continuation] Transfer completed');
        // 源设备可选择退出或保持后台
      }
    });
  }

  // 提供给插件调用的方法
  static getInstance(): EntryAbility {
    return EntryAbility.instance;
  }

  startContinuation(deviceId: string): boolean {
    try {
      continuationManager.continueAbility(deviceId);
      return true;
    } catch (err) {
      console.error('[Ability] continueAbility failed:', err);
      return false;
    }
  }
}

3. 扩展 SoftBusPlugin.ets 支持 Continuation

ts 复制代码
// plugins/SoftBusPlugin.ets
import { EntryAbility } from '../entryability/EntryAbility';

class SoftBusPlugin {
  private static instance: SoftBusPlugin;
  private appStateCallback: ((state: string) => void) | null = null;
  private restoreCallback: ((data: string) => void) | null = null;

  static getInstance(): SoftBusPlugin {
    if (!SoftBusPlugin.instance) {
      SoftBusPlugin.instance = new SoftBusPlugin();
    }
    return SoftBusPlugin.instance;
  }

  init() {
    // ... 原有 MethodChannel / EventChannel 初始化
  }

  // 【新增】Flutter 请求当前应用状态(用于流转)
  requestAppState(callback: (state: string) => void) {
    this.appStateCallback = callback;
    // 通过 EventChannel 通知 Dart 层
    if (this.eventSink) {
      this.eventSink.success({ type: 'request_app_state' });
    }
  }

  // 【新增】恢复应用状态
  restoreAppState(data: string) {
    if (this.restoreCallback) {
      this.restoreCallback(data);
    }
  }

  // 暴露给 MethodChannel 的方法
  handleMethodCall(call: any): Promise<any> {
    switch (call.method) {
      // ... 其他方法

      case 'startContinuation':
        const deviceId = call.arguments['deviceId'];
        const success = EntryAbility.getInstance().startContinuation(deviceId);
        return Promise.resolve({ success });

      case 'sendAppState': // Flutter 主动上报状态
        if (this.appStateCallback) {
          this.appStateCallback(call.arguments['state']);
          this.appStateCallback = null;
        }
        return Promise.resolve({ success: true });

      default:
        return Promise.reject('Unknown method');
    }
  }

  setOnRestore(callback: (data: string) => void) {
    this.restoreCallback = callback;
  }
}

四、Dart 侧:Flutter 应用集成

1. 扩展 ContinuationService

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

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

  // 发起流转
  static Future<bool> startContinuation(String deviceId) async {
    final result = await _method.invokeMethod('startContinuation', {'deviceId': deviceId});
    return result['success'] == true;
  }

  // 上报当前应用状态(JSON 字符串)
  static Future<void> sendAppState(String state) async {
    await _method.invokeMethod('sendAppState', {'state': state});
  }

  // 监听原生层事件(如请求状态、恢复状态)
  static Stream<Map<String, dynamic>> watchEvents() {
    return _event.receiveBroadcastStream().map((e) => e as Map);
  }
}

2. 在 UI 中触发流转

dart 复制代码
// 在设备列表页添加"流转"按钮
void _showContinuationDialog(BuildContext context, String deviceId) {
  showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: Text('流转到此设备?'),
      actions: [
        TextButton(onPressed: Navigator.of(context).pop, child: Text('取消')),
        TextButton(
          onPressed: () async {
            // 序列化当前页面状态
            final state = jsonEncode({
              'currentScreen': 'todo_edit',
              'editingTodoId': _editingId,
              'inputText': _controller.text,
            });
            await ContinuationService.sendAppState(state);
            final success = await ContinuationService.startContinuation(deviceId);
            if (success) {
              ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('正在流转...')));
            }
            Navigator.of(context).pop();
          },
          child: Text('流转'),
        )
      ],
    ),
  );
}

3. 处理状态恢复

dart 复制代码
// 在 main.dart 初始化时监听恢复事件
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 监听 Continuation 恢复
  ContinuationService.watchEvents().listen((event) {
    if (event['type'] == 'restore_app_state') {
      final data = event['data'] as String;
      final json = jsonDecode(data);
      // 根据状态跳转页面或恢复输入框内容
      GlobalAppState.restoreFromJson(json);
    }
  });

  runApp(MyApp());
}

// 全局状态管理类(简化)
class GlobalAppState {
  static String? currentScreen;
  static String? editingTodoId;
  static String? inputText;

  static void restoreFromJson(Map<String, dynamic> json) {
    currentScreen = json['currentScreen'];
    editingTodoId = json['editingTodoId'];
    inputText = json['inputText'];
  }
}

并在 MaterialApphome 中根据 GlobalAppState 跳转:

dart 复制代码
home: Builder(builder: (context) {
  if (GlobalAppState.currentScreen == 'todo_edit') {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      // 异步恢复编辑状态
      Navigator.push(context, MaterialPageRoute(builder: (_) => TodoEditPage(
        todoId: GlobalAppState.editingTodoId,
        initialText: GlobalAppState.inputText ?? '',
      )));
    });
  }
  return HomePage();
})

五、测试流程

  1. 在手机上打开 Flutter 应用,进入待办编辑页;
  2. 点击"流转"按钮,选择已配对的平板;
  3. 手机调用 continueAbility(),系统弹出确认框;
  4. 平板自动启动应用,并恢复编辑页面与输入内容;
  5. 手机端可选择退出或保持后台。

✅ 效果:用户感觉"任务从手机飞到了平板",毫无割裂感。


六、注意事项与限制

问题 说明 解决方案
Flutter 无法直接控制 Ability 生命周期 Continuation 必须由原生 Ability 发起 通过 MethodChannel 代理调用
状态序列化复杂 需手动将 Widget 状态转为 JSON 使用统一状态管理(如 Riverpod + toJson)
仅支持同应用流转 不能跨 BundleName 确保多端安装同一签名应用
设备需在线且信任 否则无法触发 提前完成设备配对

七、总结

本文实现了 Flutter 应用在 OpenHarmony 上的三大分布式能力融合

  1. 软总线:设备发现与通信;
  2. 分布式数据管理:状态持久化与同步;
  3. Continuation:任务无缝流转。

这标志着 Flutter 应用在 OpenHarmony 生态中,已具备构建真正分布式用户体验 的能力。虽然目前仍需较多桥接代码,但随着 OpenHarmony Flutter Plugin 生态 的完善,未来有望一键集成。


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

相关推荐
忆江南15 小时前
iOS 深度解析
flutter·ios
明君8799716 小时前
Flutter 实现 AI 聊天页面 —— 记一次 Markdown 数学公式显示的踩坑之旅
前端·flutter
恋猫de小郭17 小时前
移动端开发稳了?AI 目前还无法取代客户端开发,小红书的论文告诉你数据
前端·flutter·ai编程
MakeZero19 小时前
Flutter那些事-交互式组件
flutter
shankss20 小时前
pull_to_refresh_simple
flutter
shankss20 小时前
Flutter 下拉刷新库新特性:智能预加载 (enableSmartPreload) 详解
flutter
SoaringHeart2 天前
Flutter调试组件:打印任意组件尺寸位置信息 NRenderBox
前端·flutter
九狼3 天前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
_squirrel3 天前
记录一次 Flutter 升级遇到的问题
flutter
Haha_bj3 天前
Flutter——状态管理 Provider 详解
flutter·app