一、引言:从单设备到分布式协同
OpenHarmony 的核心愿景之一是 "超级终端" ------ 多个物理设备无缝协同,形成一个逻辑上的统一工作空间。例如:
- 在手机上开始编辑文档,走到平板前自动续写;
- 车机导航途中,到家后由智慧屏继续播报;
- 智能手表接收通知,点击后在电视上播放视频。
这种 "任务接力" (Task Continuity) 能力,依赖于设备间的任务注册、状态同步与用户授权 。虽然底层由 OpenHarmony 分布式软总线实现,但前端 UI 必须清晰表达任务的流转状态与操作入口。
本文将构建一个模拟页面:「跨设备任务协同中心」。它具备以下特性:
- 展示当前设备正在处理的任务(如"编辑报告"、"播放音乐");
- 列出附近可信设备(手机、平板、车机等);
- 提供"推送任务"按钮,将当前任务发送至其他设备;
- 实时反馈任务状态(已推送 / 接收中 / 已完成);
- 响应式布局:手机竖屏为列表,横屏/平板为双栏,桌面为三栏信息面板。
这不仅是一个 UI 演示,更是对 分布式场景下用户心智模型 的可视化探索。
二、完整可运行代码
dart
// lib/main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'OpenHarmony 任务协同中心',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
appBarTheme: const AppBarTheme(centerTitle: true),
),
home: const TaskContinuityPage(),
);
}
}
/// 任务状态枚举
enum TaskStatus { active, pushed, received, completed }
/// 任务类型
class Task {
final String id;
final String title;
final String description;
final IconData icon;
late TaskStatus status;
Task({
required this.id,
required this.title,
required this.description,
required this.icon,
TaskStatus? status,
}) : status = status ?? TaskStatus.active;
}
/// 设备类型(用于模拟附近设备)
class NearbyDevice {
final String id;
final String name;
final String type; // "phone", "tablet", "car", "tv"
final bool isTrusted;
const NearbyDevice({
required this.id,
required this.name,
required this.type,
this.isTrusted = true,
});
}
/// 模拟数据源
final List<Task> _mockActiveTasks = [
Task(
id: 'task_001',
title: '编辑Q3财报',
description: '使用WPS编辑季度财务报告',
icon: Icons.description,
),
Task(
id: 'task_002',
title: '播放播客',
description: '《科技早知道》第128期',
icon: Icons.podcasts,
),
];
final List<NearbyDevice> _mockNearbyDevices = [
const NearbyDevice(id: 'dev_1', name: '我的手机', type: 'phone', isTrusted: true),
const NearbyDevice(id: 'dev_2', name: '办公平板', type: 'tablet', isTrusted: true),
const NearbyDevice(id: 'dev_3', name: '家庭智慧屏', type: 'tv', isTrusted: true),
const NearbyDevice(id: 'dev_4', name: '车载系统', type: 'car', isTrusted: false),
];
class TaskContinuityPage extends StatefulWidget {
const TaskContinuityPage({super.key});
@override
State<TaskContinuityPage> createState() => _TaskContinuityPageState();
}
class _TaskContinuityPageState extends State<TaskContinuityPage> {
late List<Task> _tasks;
late List<NearbyDevice> _devices;
@override
void initState() {
super.initState();
// 深拷贝模拟数据,避免修改原始列表
_tasks = _mockActiveTasks.map((t) => Task(
id: t.id,
title: t.title,
description: t.description,
icon: t.icon,
status: t.status,
)).toList();
_devices = List.of(_mockNearbyDevices);
}
/// 模拟推送任务到设备(异步)
Future<void> _pushTaskTo(Task task, NearbyDevice device) async {
if (task.status != TaskStatus.active) return;
// 更新本地任务状态为"已推送"
setState(() {
task.status = TaskStatus.pushed;
});
// 模拟网络延迟
await Future.delayed(const Duration(seconds: 1));
// 模拟设备接收成功(实际应通过分布式软总线回调)
if (device.isTrusted) {
setState(() {
task.status = TaskStatus.received;
});
await Future.delayed(const Duration(seconds: 1));
setState(() {
task.status = TaskStatus.completed;
});
} else {
// 不可信设备,回滚状态
await Future.delayed(const Duration(seconds: 1));
setState(() {
task.status = TaskStatus.active;
});
}
}
/// 获取设备方向与尺寸,决定布局模式
bool get _isLandscapeOrLarge {
final size = MediaQuery.sizeOf(context);
return size.width > size.height || size.shortestSide >= 600;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('任务协同中心')),
body: _isLandscapeOrLarge
? _buildDualPaneLayout()
: _buildSingleColumnLayout(),
);
}
/// 单列布局(手机竖屏)
Widget _buildSingleColumnLayout() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _tasks.length,
itemBuilder: (context, index) {
final task = _tasks[index];
return Column(
children: [
_buildTaskCard(task),
const SizedBox(height: 16),
_buildDeviceSelector(task),
const SizedBox(height: 24),
],
);
},
);
}
/// 双栏/三栏布局(横屏、平板、桌面)
Widget _buildDualPaneLayout() {
return Row(
children: [
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('当前任务', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) => _buildTaskCard(_tasks[index]),
),
),
],
),
),
),
const VerticalDivider(width: 1),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('推送至设备', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
..._tasks.map((task) => Column(
children: [
Text('${task.title} →', style: const TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
_buildDeviceSelector(task),
const SizedBox(height: 20),
],
)),
],
),
),
),
],
);
}
/// 构建任务卡片
Widget _buildTaskCard(Task task) {
final statusInfo = _getStatusInfo(task.status);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(task.icon, size: 24, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(task.title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text(task.description, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusInfo.color.withOpacity(0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
statusInfo.text,
style: TextStyle(color: statusInfo.color, fontSize: 12, fontWeight: FontWeight.bold),
),
),
],
),
),
],
),
),
);
}
/// 构建设备选择器(带推送按钮)
Widget _buildDeviceSelector(Task task) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: _devices.map((device) {
final isDisabled = task.status != TaskStatus.active;
return ActionChip(
label: Text(device.name),
avatar: _getDeviceIcon(device.type),
onPressed: isDisabled
? null
: () => _pushTaskTo(task, device),
backgroundColor: isDisabled
? Colors.grey.shade200
: (device.isTrusted ? Colors.blue.shade50 : Colors.red.shade50),
disabledColor: Colors.grey.shade200,
);
}).toList(),
);
}
/// 根据设备类型返回图标
Widget _getDeviceIcon(String type) {
switch (type) {
case 'phone':
return const Icon(Icons.phone_android, size: 18);
case 'tablet':
return const Icon(Icons.tablet_android, size: 18);
case 'tv':
return const Icon(Icons.tv, size: 18);
case 'car':
return const Icon(Icons.directions_car, size: 18);
default:
return const Icon(Icons.devices, size: 18);
}
}
/// 获取任务状态显示信息
({Color color, String text}) _getStatusInfo(TaskStatus status) {
switch (status) {
case TaskStatus.active:
return (color: Colors.blue, text: '进行中');
case TaskStatus.pushed:
return (color: Colors.orange, text: '已推送');
case TaskStatus.received:
return (color: Colors.purple, text: '接收中');
case TaskStatus.completed:
return (color: Colors.green, text: '已完成');
}
}
}
✅ 此代码无需额外依赖,可直接在 Flutter 3.24+ 环境中运行,完美适配 OpenHarmony 模拟器。
三、页面创新点与设计思想解析
1. 全新交互范式:任务为中心,而非设备或能力
前三篇文章分别聚焦于:
- 设备类型识别;
- 主题切换;
- 硬件能力检测。
而本文以"任务"为核心实体,设备只是任务的载体。这更贴近 OpenHarmony "服务化"与"原子化"的设计理念------应用功能被拆解为可流转的服务单元。
2. 动态布局策略:基于方向与尺寸的智能切换
dart
bool get _isLandscapeOrLarge {
final size = MediaQuery.sizeOf(context);
return size.width > size.height || size.shortestSide >= 600;
}

- 手机竖屏 :采用
ListView单列布局,任务与设备选择器垂直堆叠,适合单手操作; - 横屏/平板/桌面 :切换为
Row双栏布局,左侧任务列表,右侧设备推送区,信息密度更高; - 不依赖固定断点 ,而是结合屏幕方向 与最小边长,更符合真实使用场景。
💡 这种"情境感知布局"比单纯按设备类型划分更智能。
四、核心组件深度剖析
1. Task 与 NearbyDevice:领域模型定义
dart
class Task {
final String id;
final String title;
final String description;
final IconData icon;
late TaskStatus status;
}
Task是有状态的业务对象 ,其status会随用户操作变化;NearbyDevice包含isTrusted字段,模拟 OpenHarmony 的设备认证机制------只有可信设备才能接收任务。
📌 这些类构成了页面的数据骨架,UI 完全由它们驱动。
2. 任务状态机:TaskStatus 与状态流转
dart
enum TaskStatus { active, pushed, received, completed }
active:任务在当前设备运行;pushed:已发出推送请求;received:目标设备确认接收;completed:任务在目标设备完成。
状态流转通过 _pushTaskTo 方法模拟:
- 可信设备:
active → pushed → received → completed; - 不可信设备:
active → pushed → active(回滚)。
✅ 这种状态机设计,为未来接入真实分布式 API 提供了清晰接口。
3. 响应式布局实现:_buildSingleColumnLayout vs _buildDualPaneLayout
- 单列布局 使用
ListView.builder,每个任务项包含卡片 + 设备选择器; - 双栏布局 使用
Row+Expanded,左侧固定任务列表,右侧动态生成"任务→设备"映射; VerticalDivider提供视觉分隔,增强信息分区。
📐 布局切换无动画过渡 (为简化),但可通过
AnimatedSwitcher优化。
4. 交互控件:ActionChip 的巧妙运用
dart
ActionChip(
label: Text(device.name),
avatar: _getDeviceIcon(device.type),
onPressed: isDisabled ? null : () => _pushTaskTo(task, device),
backgroundColor: device.isTrusted ? Colors.blue.shade50 : Colors.red.shade50,
)
ActionChip比Button更轻量,适合多选项场景;- 图标 + 文字 快速识别设备类型;
- 背景色区分可信度:蓝色=可信,红色=不可信;
- 禁用状态:当任务非 active 时,所有芯片置灰不可点。
✅ 这种设计让用户一眼识别可操作项与风险项。
5. 状态反馈:颜色编码与文本提示
dart
({Color color, String text}) _getStatusInfo(TaskStatus status) {
switch (status) {
case TaskStatus.active: return (color: Colors.blue, text: '进行中');
// ...
}
}


- 蓝色:进行中(主操作色);
- 橙色:已推送(等待中);
- 紫色:接收中(跨设备同步);
- 绿色:已完成(成功状态)。
颜色心理学在此得到应用:用户无需阅读文字,仅凭颜色即可理解状态。
五、工程价值与扩展性
1. 易于对接 OpenHarmony 分布式能力
-
将
_pushTaskTo中的Future.delayed替换为:dartfinal result = await MethodChannel('ohos/distributed_task').invokeMethod( 'pushTask', {'taskId': task.id, 'targetDeviceId': device.id}, ); -
监听分布式回调更新
Task.status。
2. 支持任务历史与撤销
- 新增
TaskHistoryPage展示已完成任务; - 在
pushed状态下提供"取消推送"按钮。
3. 多任务并发处理
- 当前一次只处理一个任务,可扩展为支持批量推送;
- 使用
Future.wait并行推送至多个设备。
4. 安全与权限提示
- 对不可信设备,点击时弹出授权对话框;
- 集成 OpenHarmony 权限管理 API。
六、用户体验思考
1. 降低认知负荷
- 用户无需理解"分布式软总线"等技术概念;
- 通过"推送至设备"这一动作,直观表达任务流转意图。
2. 提供即时反馈
- 点击后立即变色(
pushed),告知操作已受理; - 模拟进度(
received→completed),减少等待焦虑。
3. 防错设计
- 不可信设备用红色背景警示;
- 非 active 任务自动禁用操作,防止误触。
七、结语:协同是未来的 UI 范式
本文构建的「跨设备任务协同中心」,超越了传统单设备 UI 的局限,进入了 "多设备协同" 的新维度。它不仅是 OpenHarmony 分布式能力的前端体现,更是对未来人机交互方式的一次探索。
在万物互联的时代,应用不再是孤立的程序,而是流动的服务。作为开发者,我们有责任设计出能清晰表达这种流动性的界面。
🌐 欢迎加入开源鸿蒙跨平台社区 :
在这里,您将获得:
- OpenHarmony 分布式任务开发指南;
- Flutter 跨设备状态同步最佳实践;
- 实战项目模板与社区支持。
让我们携手,打造真正无缝的超级终端体验!