别再重复发明对话框了!鸿蒙FunctionComponent一把梭出智能对话浮窗

上周帮隔壁组 review 代码,看到一个小伙为了在元服务里加个"智能客服"入口,吭哧吭哧写了 200 多行:自定义弹窗动画、手写输入框焦点管理、自己维护对话状态机…我拍了拍他肩膀:“兄弟,从 HarmonyOS 4.0 开始,系统就内置了 FunctionComponent,你这属于用汇编写 React 啊。”

一、痛点:为什么你不需要自己写 AI 对话界面?

如果你正在开发鸿蒙元服务,想加个智能对话功能——比如商品导购、学习助手、AI 客服——你第一反应是不是这样?

  • 手撸全屏/半屏弹窗,调半天圆角和阴影
  • 管理一堆状态:展开/收起、键盘弹出/收起、输入框焦点
  • 处理边界情况:横竖屏切换、多任务切换、返回键逻辑
  • 纠结设计规范:要不要加拖拽条?动画曲线用哪个?

打住。
HarmonyOS 4.0 开始,系统就内置了一个专门干这事的"瑞士军刀":FunctionComponent(功能组件)。它本质上是一个预置的、符合鸿蒙设计规范的智能体对话容器。
说白了,它就是小艺那个对话界面的标准化封装。你只需要关心两件事:

  • 业务逻辑(你的 AI 能力是什么)
  • 数据(你要给用户看什么)

剩下的交互动画、键盘管理、生命周期,系统全包了。
今天,我就带你用最短的代码跑通它,再聊聊那些文档里没明说的实战技巧。

二、5 分钟极速上手:最小化 Demo

先看最简版本。文档里其实给了骨架,但有点语焉不详。咱们把它补全,变成一个真正能跑的代码。
核心就三步:

  1. 在布局里直接使用 FunctionComponent 组件
  2. 通过 controller 控制它的显示/隐藏
  3. 监听它的事件,处理业务逻辑

上代码:

// FunctionComponentMinimalDemo.ets
import { FunctionComponent, FunctionController } from '@kit.AgentFrameworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Index {
  private controller: FunctionController = new FunctionController();
  @State queryText: string = '默认的问题';
  @State isAgentSupport: boolean = false;
  @State agentId: string = 'agent0906b1e50e484e1faa0f7e9baafbf520'; // 替换为你的智能体ID

  // 检查智能体是否支持
  async checkAgentSupport() {
    try {
      let context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
      this.isAgentSupport = await this.controller.isAgentSupport(context, this.agentId);
    } catch (err) {
      hilog.error(0x0001, 'AgentExample', `err code: ${err.code}, message: ${err.message}`);
    }
  }

  aboutToAppear() {
    this.checkAgentSupport();
    this.controller?.on('agentDialogOpened', this.onAgentOpenedCallback);
    this.controller?.on('agentDialogClosed', this.onAgentClosedCallback);
  }

  aboutToDisappear() {
    this.controller?.off('agentDialogOpened');
    this.controller?.off('agentDialogClosed');
  }

  onAgentClosedCallback = () => {
    console.log('智能体对话框已关闭');
  }

  onAgentOpenedCallback = () => {
    console.log('智能体对话框已打开');
    this.queryText = `用户的提问文案`;
  }

  build() {
    Column() {
      Text(`isAgentSupport: ${this.isAgentSupport}`)
        .fontSize(16)
        .margin({ bottom: 20 });

      if (this.isAgentSupport) {
        FunctionComponent({
          controller: this.controller,
          agentId: this.agentId, // 使用绑定的智能体ID
          onError: (err) => {
            hilog.error(0x0001, 'Index', `err code: ${err.code}, message: ${err.message}`);
          },
          options: {
            title: 'AI 助手',
            controlSize: ControlSize.SMALL,
            queryText: this.queryText,
            isShowShadow: true
          }
        })
      } else {
        Text('当前设备不支持该智能体')
          .fontSize(16)
          .fontColor(Color.Red);
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

这段代码跑起来是什么效果?

  • 应用启动时会自动检查智能体支持状态
  • 如果支持,显示 FunctionComponent 智能体对话框
  • 对话框内置了输入框、发送按钮、消息展示区域
  • 对话框的打开、关闭、键盘交互、动画,全都不用你管

关键点解析:

  • FunctionController: 这是控制 FunctionComponent 显示隐藏的核心控制器
  • isAgentSupport(): 必须调用的前置检查,验证设备是否支持指定的智能体
  • agentDialogOpened / agentDialogClosed: 智能体对话框的生命周期事件监听
  • options.queryText: 可以预设对话框打开时的初始查询文本

有趣的是,这个组件最妙的地方在于它只是个容器。真正的"智能"从哪里来?有两种主流方案,咱们接着看。

三、核心玩法:如何把"智能"装进这个容器?

对话框有了,但里面是空的。怎么让它真正能对话?这里就有讲究了。

方案一:绑定智能体工作流 —— 低代码,快速上线

这是最省事的办法。如果你的 AI 逻辑是用鸿蒙智能体开发平台的工作流编排的(那个拖拽式的可视化编辑器),那么集成简单得离谱。

后端服务

鸿蒙智能体平台

鸿蒙元服务/应用

工作流节点

支持

不支持

用户界面

FunctionComponent

FunctionController

前置能力校验

显示智能体对话框

提示不支持

智能体ID绑定

云端工作流

开始

大模型

插件

知识库

代码

其他

大模型服务

插件服务

知识库检索

自定义逻辑

结果处理

结束

返回结果

盘古大模型

第三方API

鸿蒙插件

向量知识库

结果展示

你只需要在 module.json5 里声明智能体,然后在代码里指定智能体 ID:

// 在 module.json5 的 extensionAbilities 里添加
{
  "extensionAbilities": [
    {
      "name": "MyChatbotAbility",
      "type": "workflow",
      "metadata": [
        {
          "name": "ohos.extension.workflow",
          "resource": "$profile:agent_config" // 指向智能体配置文件
        }
      ]
    }
  ]
}

然后在 resources/base/profile/agent_config.json 里配置:

{
  "agents": [
    {
      "name": "MyShoppingAssistant",
      "description": "智能购物助手",
      "workflowId": "your_workflow_id_here", // 在智能体平台创建的工作流ID
      "triggerMode": "function_component"
    }
  ]
}

最后,在代码里把 FunctionComponent 和这个智能体绑定:

FunctionComponent({
  controller: this.controller,
  agentId: 'MyShoppingAssistant' // 对应 agent_config.json 里的 name
})

这样,当用户发送消息时,消息会自动路由到你配置的云端工作流。工作流里可以串接大模型节点、知识库检索节点(见文档《大模型节点-工作流节点说明》)、插件节点(调用外部 API),最终把结果直接呈现在对话框里。

优点:开发极快,UI和交互完全标准化,适合电商导购、客服 FAQ 等标准化场景。

坑点:逻辑在云端工作流编排,调试和迭代需要登录平台操作。

方案二:直连你的后端 AI 服务 —— 全代码控制

如果你的 AI 服务是自己后端部署的模型,或者需要复杂的本地逻辑,那就得自己处理消息收发了。

这时,FunctionComponent 主要提供 UI 容器,消息传输需要你自己实现。通常需要结合 @ohos.net.http 或其他通信方式。

import { http } from '@kit.NetworkKit';
import { FunctionComponent, FunctionController } from '@kit.AgentFrameworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Component
struct MyAIChat {
  private controller: FunctionController = new FunctionController();
  @State currentReply: string = '';
  @State isAgentSupport: boolean = false;
  @State agentId: string = 'your_custom_agent_id';

  aboutToAppear() {
    this.checkAgentSupport();
  }

  async checkAgentSupport() {
    try {
      let context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
      this.isAgentSupport = await this.controller.isAgentSupport(context, this.agentId);
    } catch (err) {
      hilog.error(0x0001, 'MyAIChat', `err code: ${err.code}, message: ${err.message}`);
    }
  }

  build() {
    Column() {
      if (this.isAgentSupport) {
        FunctionComponent({
          controller: this.controller,
          agentId: this.agentId,
          onError: (err) => {
            hilog.error(0x0001, 'MyAIChat', `err code: ${err.code}, message: ${err.message}`);
          },
          options: {
            title: '自定义AI助手',
            controlSize: ControlSize.SMALL,
            queryText: '有什么可以帮您?',
            isShowShadow: true
          }
        })
        .on('agentDialogOpened', () => {
          console.log('对话框已打开,可以开始对话');
        })
        .on('agentDialogClosed', () => {
          console.log('对话框已关闭');
        })
      }

      // 显示AI回复(示例用,实际回复应显示在FunctionComponent内)
      Text(this.currentReply)
        .fontSize(14)
        .margin({ top: 20 })
        .padding(10)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .border({ width: 1, color: Color.Gray })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Start)
  }

  // 调用后端AI服务的示例方法
  private async callMyAIBackend(query: string): Promise<string> {
    let response: string = '服务暂时不可用';
    try {
      const httpResponse = await http.createHttp().request(
        ' https://your-ai-service.com/chat',
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify({ question: query })
        }
      );
      if (httpResponse.responseCode === 200) {
        const result = JSON.parse(httpResponse.result.toString());
        response = result.answer;
      }
    } catch (err) {
      console.error(`调用AI服务失败: ${JSON.stringify(err)}`);
    }
    return response;
  }
}

重要提醒:在纯代码对接方案中,FunctionComponent 主要扮演一个标准化输入窗口的角色。将AI回复实时、无缝地显示到其内部对话气泡中,通常需要通过智能体工作流或插件机制。如果文档未明确提供直接发送消息的API,那么方案一(绑定工作流)是更官方、更完整的路径。

优点:架构灵活,可对接任何 AI 服务。

坑点:需要自己处理网络、错误、加载状态,且可能无法充分利用 FunctionComponent 的全部内置对话能力。

四、高级技巧:状态、自定义与性能

1. 状态持久化:让对话有记忆

FunctionComponent 默认不保存历史记录。但用户肯定希望下次打开时,能看到之前的聊天记录。

你可以在 agentDialogClosed 回调里,把对话记录存到本地数据库或 Preferences 里:

import { dataPreferences } from '@kit.ArkData';

// 在组件中
aboutToAppear() {
  this.controller?.on('agentDialogClosed', this.onAgentClosedCallback);
  this.controller?.on('agentDialogOpened', this.onAgentOpenedCallback);
}

onAgentOpenedCallback = async () => {
  // 打开时,尝试读取历史记录
  try {
    const historyStr = await dataPreferences.get('chat_history', '[]');
    const history = JSON.parse(historyStr);
    // 注意:将历史记录设置到对话框需要通过智能体工作流的"长期记忆节点"实现
    // 或者通过预设queryText展示最后一条消息
    if (history.length > 0) {
      this.queryText = `继续上次对话:${history[history.length - 1].content}`;
    }
    console.log('加载历史记录:', history.length, '条');
  } catch (err) {
    console.error('读取历史失败:', err);
  }
}

onAgentClosedCallback = async () => {
  // 关闭时,保存当前会话记录
  // 注意:实际需要从智能体工作流或自己维护的对话记录中获取
  const sessionHistory = this.getCurrentSessionHistory(); // 假设的方法
  try {
    await dataPreferences.put('chat_history', JSON.stringify(sessionHistory));
  } catch (err) {
    console.error('保存历史失败:', err);
  }
}

2. 性能与资源:别让 AI 拖垮你的元服务

如果集成了本地 AI 模型(比如用 MindSpore Lite 跑端侧小模型),要特别注意资源管理:

  • 懒加载模型:别在 aboutToAppear 里加载大模型,等第一次需要推理时再加载。
  • 及时释放:在 aboutToDisappear 或页面销毁时,释放模型和引擎。
  • 后台策略:用户切到后台时,考虑暂停耗资源的推理任务。
aboutToDisappear() {
  // 释放AI推理资源
  if (this.localModel) {
    this.localModel.release();
    this.localModel = null;
  }
  // 取消网络请求等
  this.currentRequest?.abort();
  // 移除事件监听
  this.controller?.off('agentDialogOpened');
  this.controller?.off('agentDialogClosed');
}

五、实战踩坑与选择

  • 选型决策:如果你的 AI 逻辑简单、追求快速上线,无脑选方案一(工作流)。如果 AI 逻辑极度定制、或已有成熟后端,再考虑方案二。
  • 事件顺序agentDialogOpened 回调触发时,对话框的 UI 可能还未完全渲染完毕。要操作内部元素的话,可能需要加个微小的延迟(setTimeout 或 nextTick)。
  • 设计限制FunctionComponent 的 UI 是系统强定义的,为了保证跨应用体验一致,自定义空间有限(比如改颜色、布局大动)。如果非要高度定制,可能得回到手写弹窗的老路。
  • 工作流调试:方案一的工作流在云端,真机调试前多用模拟器预览。复杂逻辑可以拆成多个子工作流,方便测试。
  • 智能体支持检查:必须调用 isAgentSupport() 方法检查设备是否支持指定的智能体,否则可能导致功能不可用。

写在最后

FunctionComponent 这个组件,代表了鸿蒙对 AI 交互的一种思路:把通用的、标准的交互容器化,让开发者只聚焦业务差异化部分。

它可能不像自己从头撸那么"自由",但在商业项目里,稳定、一致、快速交付、未来兼容性,才是更重要的。下次当你又想手写一个对话浮窗时,先问问自己:

“这个功能,是不是又造了一个系统已经造好、而且未来会持续优化的轮子?”


毕竟,咱们的时间应该花在让 AI 更聪明、更懂业务上,而不是反复调试对话框的动画曲线和键盘冲突。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐