在鸿蒙生态的全场景交互体系中,服务卡片是连接用户与应用的轻量化入口,它无需启动完整应用,即可在桌面、负一屏等位置展示核心信息并提供快捷操作。Flutter 凭借跨端一致的 UI 渲染能力和灵活的组件化架构,能够高效开发适配多设备的鸿蒙服务卡片。本文将聚焦鸿蒙 Flutter 服务卡片的开发流程桌面智能交互优化,通过 “个人日程助手” 服务卡片案例,演示如何实现卡片数据实时更新、用户交互响应、跨设备卡片流转等核心能力。

一、鸿蒙服务卡片核心特性与 Flutter 适配逻辑

1. 鸿蒙服务卡片核心特性

鸿蒙服务卡片(Service Card)是基于鸿蒙Form框架的轻量化交互组件,具备四大核心特性:

  • 轻量化展示:卡片体积小巧(通常为几 KB 到几十 KB),可在桌面常驻,实时展示应用核心数据(如天气、日程、待办);
  • 快捷交互:支持点击、滑动等基础交互,用户无需打开应用即可完成核心操作(如标记待办完成、切换音乐);
  • 多形态适配:支持网格型、列表型、瀑布流型等多种布局,适配手机、平板、智慧屏等不同设备的桌面尺寸;
  • 数据实时同步:通过鸿蒙FormProvider实现卡片数据与应用数据的双向同步,数据变更可实时推送到卡片界面;
  • 跨设备流转:支持卡片在同一账号下的多设备间流转,例如将手机上的日程卡片流转至平板桌面。

2. Flutter 与服务卡片的适配逻辑

Flutter 开发鸿蒙服务卡片遵循 “UI 渲染与数据驱动分离、原生能力桥接、多形态自适应” 三大原则,整体架构分为三层:

  1. 鸿蒙原生 Form 层:基于FormAbilityFormProvider实现卡片的创建、更新、销毁生命周期管理,负责与系统桌面通信;
  2. Flutter 卡片 UI 层:通过FlutterForm组件实现卡片的可视化布局,支持多形态尺寸适配,接收原生层传递的数据并渲染;
  3. 数据同步层:通过MethodChannelEventChannel实现 Flutter 卡片与原生 Form 层的数据双向通信,确保卡片数据与应用数据实时一致。

3. 服务卡片数据流转机制

以 “日程卡片更新” 为例,完整的数据流转流程如下:

  1. 数据变更:用户在 Flutter 应用中修改日程信息 → 应用通过MethodChannel通知原生FormProvider
  2. 数据推送FormProvider将更新后的日程数据封装为FormData,通过鸿蒙系统 API 推送到桌面卡片;
  3. UI 刷新:原生FormAbility接收数据并通过EventChannel传递给 Flutter 卡片 UI 层 → 卡片界面自动刷新展示新数据;
  4. 交互响应:用户点击卡片上的 “完成” 按钮 → Flutter 卡片通过MethodChannel通知原生层 → 原生层调用应用接口完成状态修改。

二、案例:个人日程助手服务卡片

本案例将实现一款基于 Flutter 的鸿蒙个人日程助手服务卡片,核心功能包括:

  1. 多形态卡片:支持 2×1 网格型(展示今日日程概览)和 2×2 列表型(展示未来 3 天日程)两种形态;
  2. 数据实时同步:应用内修改日程后,卡片数据自动更新,无需手动刷新;
  3. 快捷交互:卡片上直接标记日程完成、删除日程,操作结果同步回应用;
  4. 跨设备流转:支持将手机上的日程卡片流转至平板桌面,保持数据一致;
  5. 智能提醒:日程即将开始时,卡片自动变色提醒用户。

前置条件

  1. 已配置鸿蒙 DevEco Studio 4.3 + 与 Flutter 3.24 + 环境,安装ohos_flutter_form插件;
  2. 已掌握鸿蒙Form框架基础概念,了解FormAbilityFormProvider的使用方法;
  3. 已准备至少两台鸿蒙设备(手机 + 平板),登录同一鸿蒙账号并开启分布式软总线;
  4. 已掌握 FlutterStreamChangeNotifier状态管理方案。

三、步骤 1:鸿蒙原生层 Form 能力封装

鸿蒙原生层负责实现服务卡片的生命周期管理、数据同步和交互响应,为 Flutter 卡片提供底层支撑。

1. 服务卡片配置(module.json5)

entry/src/main/module.json5中配置服务卡片的类型、尺寸、权限等核心信息:

{
  "module": {
    "reqPermissions": [
      {
        "name": "ohos.permission.GET_BUNDLE_INFO",
        "reason": "获取应用信息用于卡片创建",
        "usedScene": { "abilities": [".FormAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "同步云端日程数据",
        "usedScene": { "abilities": [".FormAbility"], "when": "inuse" }
      }
    ],
    "abilities": [
      {
        "name": ".MainAbility",
        "type": "page",
        "visible": true,
        "metadata": [
          {
            "name": "flutterAbility",
            "value": "true"
          }
        ]
      },
      {
        "name": ".FormAbility",
        "type": "form",
        "visible": true,
        "exported": true,
        "metadata": [
          {
            "name": "forms",
            "value": {
              "supportDimensions": ["2*1", "2*2"],
              "defaultDimension": "2*1",
              "name": "日程助手卡片",
              "description": "展示今日日程并提供快捷操作",
              "icon": "$media:form_icon",
              "type": "realtime"
            }
          },
          {
            "name": "flutterAbility",
            "value": "true"
          }
        ]
      }
    ]
  }
}

2. 日程数据模型定义(ArkTS)

定义与 Flutter 层一致的日程数据模型,确保数据序列化 / 反序列化的一致性:

// model/ScheduleModel.ets
export interface ScheduleItem {
  id: string;
  title: string;
  time: string; // 格式:HH:mm
  isCompleted: boolean;
  isRemind: boolean;
}

export interface FormScheduleData {
  items: ScheduleItem[];
  updateTime: string;
}

3. Form 能力核心封装(ArkTS)

实现FormAbility管理卡片生命周期,通过FormProvider完成数据同步,同时处理卡片交互事件:

// ability/FormAbility.ets
import Ability from '@ohos.app.ability.FormAbility';
import formProvider from '@ohos.app.ability.formProvider';
import formInfo from '@ohos.app.ability.formInfo';
import Want from '@ohos.app.ability.Want';
import { FormScheduleData, ScheduleItem } from '../model/ScheduleModel';
import { Logger } from '../utils/Logger';

export default class FormAbility extends Ability {
  // 模拟日程数据
  private defaultScheduleData: FormScheduleData = {
    items: [
      { id: '1', title: '项目评审会', time: '09:30', isCompleted: false, isRemind: true },
      { id: '2', title: '客户对接', time: '14:00', isCompleted: false, isRemind: false }
    ],
    updateTime: new Date().toLocaleTimeString()
  };

  // 卡片创建时触发
  onCreateForm(want: Want): formInfo.FormData {
    Logger.info('FormAbility onCreateForm');
    const formId = want.parameters?.formId as string;
    // 初始化卡片数据
    this.updateFormData(formId, this.defaultScheduleData);
    return this.defaultScheduleData;
  }

  // 卡片更新时触发
  onUpdateForm(formId: string): void {
    Logger.info(`FormAbility onUpdateForm, formId: ${formId}`);
    // 模拟数据更新:添加一条新日程
    this.defaultScheduleData.items.push({
      id: '3',
      title: '团队周会',
      time: '16:30',
      isCompleted: false,
      isRemind: true
    });
    this.defaultScheduleData.updateTime = new Date().toLocaleTimeString();
    this.updateFormData(formId, this.defaultScheduleData);
  }

  // 卡片被删除时触发
  onDestroyForm(formId: string): void {
    Logger.info(`FormAbility onDestroyForm, formId: ${formId}`);
  }

  // 处理卡片点击事件
  onTriggerFormEvent(formId: string, message: string): void {
    Logger.info(`FormAbility onTriggerFormEvent, formId: ${formId}, message: ${message}`);
    const action = JSON.parse(message);
    // 根据用户操作更新数据
    if (action.type === 'complete') {
      const item = this.defaultScheduleData.items.find(i => i.id === action.id);
      if (item) {
        item.isCompleted = !item.isCompleted;
        this.updateFormData(formId, this.defaultScheduleData);
      }
    } else if (action.type === 'delete') {
      this.defaultScheduleData.items = this.defaultScheduleData.items.filter(i => i.id !== action.id);
      this.updateFormData(formId, this.defaultScheduleData);
    }
  }

  // 更新卡片数据
  private updateFormData(formId: string, data: FormScheduleData): void {
    formProvider.updateForm(formId, data).then(() => {
      Logger.info('Form data updated successfully');
    }).catch((err) => {
      Logger.error(`Failed to update form data: ${JSON.stringify(err)}`);
    });
  }

  // 卡片被激活时触发(如用户点击卡片)
  onAcquireFormState(want: Want): formInfo.FormState {
    return formInfo.FormState.READY;
  }
}

4. Flutter 与 Form 通信桥接(ArkTS)

通过MethodChannelEventChannel实现 Flutter 卡片与原生 Form 层的数据交互和事件传递:

// ability/FormCommunication.ets
import { FormScheduleData } from '../model/ScheduleModel';
import { MethodChannel, EventChannel } from '@ohos.flutter.engine';

export class FormCommunication {
  private static instance: FormCommunication;
  private formDataChannel?: EventChannel;
  private formEventChannel?: MethodChannel;
  private formDataCallback?: (data: FormScheduleData) => void;

  private constructor() {}

  // 单例模式
  public static getInstance(): FormCommunication {
    if (!FormCommunication.instance) {
      FormCommunication.instance = new FormCommunication();
    }
    return FormCommunication.instance;
  }

  // 初始化通信通道
  init(flutterEngine: any): void {
    // 1. 数据传递通道:原生→Flutter
    this.formDataChannel = new EventChannel(flutterEngine.dartExecutor.binaryMessenger, 'com.schedule.form.data');
    this.formDataChannel.setStreamHandler({
      onListen: (_, eventSink) => {
        this.formDataCallback = (data) => {
          eventSink.success(data);
        };
      },
      onCancel: () => {
        this.formDataCallback = undefined;
      }
    });

    // 2. 事件传递通道:Flutter→原生
    this.formEventChannel = new MethodChannel(flutterEngine.dartExecutor.binaryMessenger, 'com.schedule.form.event');
    this.formEventChannel.setMethodCallHandler((call, result) => {
      switch (call.method) {
        case 'triggerFormEvent':
          const formId = call.arguments['formId'] as string;
          const message = call.arguments['message'] as string;
          // 调用FormAbility的事件处理方法
          this.triggerFormEvent(formId, message);
          result.success(true);
          break;
        default:
          result.notImplemented();
      }
    });
  }

  // 发送数据到Flutter卡片
  sendFormData(data: FormScheduleData): void {
    this.formDataCallback?.(data);
  }

  // 触发卡片事件
  private triggerFormEvent(formId: string, message: string): void {
    // 实际项目中需通过AbilityContext获取FormAbility实例并调用方法
    Logger.info(`Trigger form event: ${formId}, ${message}`);
  }
}

四、步骤 2:Flutter 层服务卡片 UI 与交互实现

Flutter 层负责实现服务卡片的多形态 UI 布局、数据监听和交互响应,适配不同尺寸的卡片展示需求。

1. 数据模型与通信工具类封装(Dart)

定义与原生层一致的日程数据模型,封装通信通道接口,实现数据监听和事件触发:

// lib/models/schedule_model.dart
class ScheduleItem {
  final String id;
  final String title;
  final String time;
  final bool isCompleted;
  final bool isRemind;

  ScheduleItem({
    required this.id,
    required this.title,
    required this.time,
    required this.isCompleted,
    required this.isRemind,
  });

  factory ScheduleItem.fromJson(Map<String, dynamic> json) {
    return ScheduleItem(
      id: json['id'],
      title: json['title'],
      time: json['time'],
      isCompleted: json['isCompleted'],
      isRemind: json['isRemind'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'time': time,
      'isCompleted': isCompleted,
      'isRemind': isRemind,
    };
  }
}

class FormScheduleData {
  final List<ScheduleItem> items;
  final String updateTime;

  FormScheduleData({
    required this.items,
    required this.updateTime,
  });

  factory FormScheduleData.fromJson(Map<String, dynamic> json) {
    return FormScheduleData(
      items: (json['items'] as List).map((e) => ScheduleItem.fromJson(e)).toList(),
      updateTime: json['updateTime'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'items': items.map((e) => e.toJson()).toList(),
      'updateTime': updateTime,
    };
  }
}
// lib/services/form_communication_service.dart
import 'dart:convert';
import 'package:flutter/services.dart';
import '../models/schedule_model.dart';

class FormCommunicationService {
  static const EventChannel _formDataChannel = EventChannel('com.schedule.form.data');
  static const MethodChannel _formEventChannel = MethodChannel('com.schedule.form.event');

  // 监听卡片数据更新
  static Stream<FormScheduleData> get formDataStream {
    return _formDataChannel.receiveBroadcastStream().map((data) {
      return FormScheduleData.fromJson(Map<String, dynamic>.from(data as Map));
    });
  }

  // 触发卡片交互事件
  static Future<bool> triggerFormEvent({
    required String formId,
    required String actionType,
    required String itemId,
  }) async {
    final message = jsonEncode({
      'type': actionType,
      'id': itemId,
    });
    return await _formEventChannel.invokeMethod(
      'triggerFormEvent',
      {
        'formId': formId,
        'message': message,
      },
    );
  }
}

2. 多形态卡片 UI 实现(Dart)

根据卡片尺寸(2×1、2×2)实现不同的布局样式,支持日程完成标记、删除等快捷操作:

// lib/widgets/schedule_form_widget.dart
import 'package:flutter/material.dart';
import '../models/schedule_model.dart';
import '../services/form_communication_service.dart';

class ScheduleFormWidget extends StatefulWidget {
  final String formId;
  final String dimension; // 卡片尺寸:2*1 / 2*2

  const ScheduleFormWidget({
    super.key,
    required this.formId,
    required this.dimension,
  });

  @override
  State<ScheduleFormWidget> createState() => _ScheduleFormWidgetState();
}

class _ScheduleFormWidgetState extends State<ScheduleFormWidget> {
  late FormScheduleData _scheduleData;

  @override
  void initState() {
    super.initState();
    // 监听卡片数据更新
    FormCommunicationService.formDataStream.listen((data) {
      setState(() {
        _scheduleData = data;
      });
    });
    // 初始化默认数据
    _scheduleData = FormScheduleData(
      items: [],
      updateTime: DateTime.now().toLocal().toString(),
    );
  }

  // 标记日程完成/未完成
  void _onCompleteToggle(ScheduleItem item) async {
    await FormCommunicationService.triggerFormEvent(
      formId: widget.formId,
      actionType: 'complete',
      itemId: item.id,
    );
  }

  // 删除日程
  void _onDelete(ScheduleItem item) async {
    await FormCommunicationService.triggerFormEvent(
      formId: widget.formId,
      actionType: 'delete',
      itemId: item.id,
    );
  }

  // 构建2×1尺寸卡片
  Widget _build2x1Form() {
    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.blue),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '今日日程',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: _scheduleData.items.isEmpty
                ? const Center(child: Text('暂无日程'))
                : ListView.builder(
                    itemCount: _scheduleData.items.length,
                    itemBuilder: (context, index) {
                      final item = _scheduleData.items[index];
                      return ListTile(
                        leading: Checkbox(
                          value: item.isCompleted,
                          onChanged: (_) => _onCompleteToggle(item),
                        ),
                        title: Text(
                          item.title,
                          style: TextStyle(
                            decoration: item.isCompleted ? TextDecoration.lineThrough : null,
                            color: item.isRemind ? Colors.red : null,
                          ),
                        ),
                        trailing: Text(item.time),
                      );
                    },
                  ),
          ),
          Text(
            '更新时间:${_scheduleData.updateTime}',
            style: const TextStyle(fontSize: 10, color: Colors.grey),
          ),
        ],
      ),
    );
  }

  // 构建2×2尺寸卡片
  Widget _build2x2Form() {
    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.blue),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '日程助手',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                childAspectRatio: 2,
                crossAxisSpacing: 4,
                mainAxisSpacing: 4,
              ),
              itemCount: _scheduleData.items.length,
              itemBuilder: (context, index) {
                final item = _scheduleData.items[index];
                return GestureDetector(
                  onTap: () => _onCompleteToggle(item),
                  onLongPress: () => _onDelete(item),
                  child: Container(
                    padding: const EdgeInsets.all(4),
                    decoration: BoxDecoration(
                      color: item.isCompleted ? Colors.grey[200] : Colors.white,
                      borderRadius: BorderRadius.circular(4),
                      border: Border.all(color: item.isRemind ? Colors.red : Colors.grey),
                    ),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          item.title,
                          style: TextStyle(
                            fontSize: 12,
                            decoration: item.isCompleted ? TextDecoration.lineThrough : null,
                          ),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                        const SizedBox(height: 2),
                        Text(
                          item.time,
                          style: const TextStyle(fontSize: 10, color: Colors.grey),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
          Text(
            '更新时间:${_scheduleData.updateTime}',
            style: const TextStyle(fontSize: 10, color: Colors.grey),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return widget.dimension == '2*1' ? _build2x1Form() : _build2x2Form();
  }
}

3. 卡片入口与应用主界面实现(Dart)

实现应用主界面,支持创建不同尺寸的服务卡片,并将卡片添加到桌面:

// lib/main.dart
import 'package:flutter/material.dart';
import 'widgets/schedule_form_widget.dart';

void main() {
  runApp(const ScheduleFormApp());
}

class ScheduleFormApp extends StatelessWidget {
  const ScheduleFormApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '日程助手服务卡片',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const ScheduleFormHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class ScheduleFormHomePage extends StatefulWidget {
  const ScheduleFormHomePage({super.key});

  @override
  State<ScheduleFormHomePage> createState() => _ScheduleFormHomePageState();
}

class _ScheduleFormHomePageState extends State<ScheduleFormHomePage> {
  String _selectedDimension = '2*1';
  final String _formId = 'schedule_form_001';

  // 模拟添加卡片到桌面
  void _addToHomeScreen() {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('已添加${_selectedDimension}尺寸卡片到桌面')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('日程助手'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 卡片尺寸选择
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('选择卡片尺寸:'),
                const SizedBox(width: 16),
                DropdownButton<String>(
                  value: _selectedDimension,
                  items: const [
                    DropdownMenuItem(value: '2*1', child: Text('2×1 网格型')),
                    DropdownMenuItem(value: '2*2', child: Text('2×2 列表型')),
                  ],
                  onChanged: (value) {
                    if (value != null) {
                      setState(() {
                        _selectedDimension = value;
                      });
                    }
                  },
                ),
              ],
            ),
            const SizedBox(height: 20),
            // 卡片预览
            Expanded(
              child: ScheduleFormWidget(
                formId: _formId,
                dimension: _selectedDimension,
              ),
            ),
            const SizedBox(height: 20),
            // 添加到桌面按钮
            ElevatedButton(
              onPressed: _addToHomeScreen,
              child: const Text('添加到桌面'),
            ),
          ],
        ),
      ),
    );
  }
}

五、服务卡片核心优化与体验提升

1. 数据同步优化

  • 增量更新:仅同步变更的日程数据,而非全量数据,减少数据传输开销;
  • 定时刷新策略:根据日程的时间特性,设置卡片定时刷新(如每小时刷新一次),避免频繁更新消耗资源;
  • 离线缓存:卡片数据本地缓存,设备离线时仍可展示历史数据,联网后自动同步最新内容。

2. 交互体验优化

  • 手势交互增强:支持滑动删除日程、长按拖动调整日程顺序等手势操作;
  • 视觉反馈优化:日程完成时添加打勾动画,提醒事项用红色边框高亮,更新数据时添加淡入淡出过渡效果;
  • 适配深色模式:根据系统深色模式自动切换卡片背景色和文字颜色,提升夜间使用体验。

3. 多设备适配优化

  • 尺寸自适应:通过MediaQuery获取设备屏幕密度,自动调整卡片内文字大小和间距;
  • 设备类型适配:手机卡片以紧凑布局为主,平板和智慧屏卡片采用更宽松的布局,展示更多信息;
  • 跨设备流转适配:卡片流转时自动根据目标设备的屏幕尺寸切换最佳展示形态。

六、扩展场景与进阶建议

  1. 卡片联动:将日程卡片与日历应用、闹钟应用联动,日程开始前自动触发闹钟提醒;
  2. 个性化定制:支持用户自定义卡片背景色、字体大小、展示字段,满足个性化需求;
  3. 云端同步:接入鸿蒙云服务,实现日程数据的云端备份与多设备同步;
  4. 语音交互:集成鸿蒙语音助手,支持语音指令 “添加明天上午 9 点的会议” 直接更新卡片数据;
  5. 场景化推荐:根据用户的日程习惯,智能推荐相似的日程模板(如 “周会”“客户对接”)。

七、总结

本文通过个人日程助手服务卡片案例,完整演示了鸿蒙 Flutter 服务卡片的开发流程与核心优化技巧。核心在于利用鸿蒙Form框架的生命周期管理能力,结合 Flutter 的跨端 UI 渲染优势,实现卡片数据的实时同步与快捷交互。

在鸿蒙全场景生态中,服务卡片是提升用户粘性的关键入口,而 Flutter 则为服务卡片的跨设备适配提供了高效的开发方案。开发者可基于本文思路,探索更多服务卡片场景,如天气卡片、待办卡片、音乐卡片等,为用户打造轻量化、智能化的桌面交互体验。

欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐