HarmonyOS 做桌面卡片 onAddForm 要同步返回但数据在数据库里的解法

给一个极简日记 app 加桌面服务卡片(Form Widget)时撞到一个很矛盾的事:用户长按桌面添加卡片的瞬间,卡片内容应该已经是今天的日记进度 ------走数据库查 session + 本地数据这个过程不少于几十毫秒。但 onAddForm 这个回调要求同步返回 ,没法 await 异步 DB 查询。查了半天官方文档 + 其他开发者的坑,才摸出"先返回默认数据 → 异步 updateForm"这个标准解法。记录一下,顺便分享给有需要的朋友。

一、FormExtensionAbility 的同步/异步矛盾

鸿蒙的桌面服务卡片靠 FormExtensionAbility 实现。生命周期回调有几个:

typescript 复制代码
import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility'
import { formBindingData, formProvider } from '@kit.FormKit'

export default class EntryFormAbility extends FormExtensionAbility {
  onAddForm(want: Want): formBindingData.FormBindingData {
    // 👈 必须同步返回一个 FormBindingData
    // 用户添加卡片的瞬间触发
  }

  onUpdateForm(formId: string): void {
    // 系统刷新或主包主动触发
  }

  onRemoveForm(formId: string): void {
    // 用户移除卡片
  }
}

关键矛盾:

onAddForm 是同步函数 ,返回类型是 formBindingData.FormBindingData(不是 Promise)。意味着不能在里面 await DB 查询。

但真实的卡片数据往往需要异步才能拿到:

  • relationalStore 的 query 是 Promise
  • preferences 的 get 是 Promise
  • fileIo 读文件也可以是 Promise

朴素尝试:

typescript 复制代码
// 这写法不对
async onAddForm(want: Want) {    // ❌ 编译器会报错或 runtime 行为未定义
  const data = await this.entryDao.getByDate(today)
  return formBindingData.createFormBindingData(JSON.stringify(data))
}

就算编译器勉强让 async 通过,鸿蒙系统也不会等------卡片立刻需要内容显示在桌面上。

二、标准解法:先返回默认数据 + 异步 updateForm

真正的做法是两段:

typescript 复制代码
export default class EntryFormAbility extends FormExtensionAbility {
  private entryDao: EntryDao = new EntryDao()
  private wordStatsDao: WordStatsDao = new WordStatsDao()

  onAddForm(want: Want): formBindingData.FormBindingData {
    // 第一段:立刻返回默认数据,让卡片有东西显示
    const defaultData: FormData = {
      date: '',
      progress: 0,
      isComplete: false,
      words: [] as string[],
      recentWords: [] as string[],
    }
    const formData = formBindingData.createFormBindingData(JSON.stringify(defaultData))

    // 第二段:异步跑一次 updateForm,拉真实数据
    const formId = want.parameters?.['ohos.extra.param.key.form_identity'] as string
    if (formId) {
      this.updateFormAsync(formId)   // 👈 不 await,fire and forget
    }

    return formData   // ← 同步返回默认数据
  }

  onUpdateForm(formId: string): void {
    this.updateFormAsync(formId)
  }

  private async updateFormAsync(formId: string): Promise<void> {
    try {
      await DbHelper.getInstance().init(this.context)
      const data = await this.getFormData()
      const bindingData = formBindingData.createFormBindingData(JSON.stringify(data))
      await formProvider.updateForm(formId, bindingData)
    } catch (e) {
      // 异步失败不致命------下个 update cycle 还会再跑
    }
  }

  private async getFormData(): Promise<FormData> {
    const today = DateUtils.today()
    const entry = await this.entryDao.getByDate(today)
    // ... 真实的 DB 查询
  }
}

流程:

arduino 复制代码
用户长按桌面 → 选"添加卡片" → 系统调 onAddForm
    ↓
onAddForm 立刻返回默认 FormData(空字段,progress=0)
    ↓
卡片显示在桌面(显示的是默认数据,有点"占位态")
    ↓
updateFormAsync 开始跑(在同一个 FormExtensionAbility 进程里)
    ↓
DB 查到今日数据
    ↓
formProvider.updateForm(formId, newData) → 卡片内容刷新

用户看到的是:"添加卡片时它是空的 → 一两秒后内容自动填上"。可以接受。

为什么不用 Loading 态 :桌面卡片的尺寸很小(2x2 / 2x4),没有 Loading UI 的空间。默认数据直接是"占位态"(空日期、progress=0、空词列表),用户看到觉得是"今天还没写"而不是"加载中"。

三、从 want.parameters 拿 formId

onAddForm 的参数 want 里带了 formId,需要从 parameters 里提取:

typescript 复制代码
const formId = want.parameters?.['ohos.extra.param.key.form_identity'] as string

几个坑:

key 是常量字符串 'ohos.extra.param.key.form_identity' 。全写、全小写、带点号。鸿蒙的常量 key 都是这种长字符串格式。可以用 formInfo.FormParam.IDENTITY_KEY 这个常量代替裸字符串(更可读,但本质一样)。

parameters 可能是 undefined 。用 ?. 可选链保护。真实场景下 parameters 一般不会缺,但真缺了(API bug / 边界情况)不要让整个 app 崩。

as string 强转 。want.parameters 的类型是 Record<string, Object>,取出来的值类型是 Object。要转成 string 才能传给 formProvider。

formId 可能是空 。拿到空 formId 后调 updateForm 会报错------所以上面代码有 if (formId) 保护。

四、formProvider.updateForm 刷新

异步跑完数据查询,调 formProvider.updateForm 把新数据推给卡片:

typescript 复制代码
import { formProvider } from '@kit.FormKit'

await formProvider.updateForm(formId, bindingData)

要点:

formId 和 onAddForm 时拿到的是同一个。系统会把这个数据推给 formId 对应的卡片实例。如果用户有多个卡片(同一个 app,不同 widget 实例),每个 formId 不一样。

updateForm 是异步的,必须 await。不 await 的话,updateForm 还没完就被 GC 掉的边界 case 有可能数据漏推。

失败不致命 :try/catch 吞错。桌面系统有自己的 update cycle(默认 30 分钟一次),这次没推成功下次系统还会再触发 onUpdateForm。

五、onUpdateForm 复用同一个 updateFormAsync

系统会定期调 onUpdateForm(鸿蒙的 default refresh period 是 30 分钟)。或者主包想主动刷新卡片,可以用 formProvider.requestForm(formId) 主动触发。

无论哪种触发,最终都走到同一个 updateFormAsync:

typescript 复制代码
onUpdateForm(formId: string): void {
  this.updateFormAsync(formId)
}

优点:onAddForm 和 onUpdateForm 共享同一套数据查询逻辑,不用写两遍。

注意 :onUpdateForm 的签名也是 同步 void ,不能直接 async。做法还是同步调用一个异步函数但不 await。

六、LocalStorage + @LocalStorageProp 做卡片 UI

拿到 bindingData 之后,卡片 UI 怎么消费?用 LocalStorage + @LocalStorageProp:

卡片 UI(在 widget/pages/Widget2x2.ets):

typescript 复制代码
@Entry
@Component
struct Widget2x2 {
  @LocalStorageProp('date') date: string = ''
  @LocalStorageProp('progress') progress: number = 0
  @LocalStorageProp('isComplete') isComplete: boolean = false
  @LocalStorageProp('words') words: string[] = []

  build() {
    Column() {
      Text(this.date).fontSize(14)
      if (this.isComplete) {
        Row() {
          ForEach(this.words, (word: string) => {
            Text(word).fontSize(12)
          })
        }
      } else {
        Text(`${this.progress}/6`).fontSize(32)
      }
    }
  }
}

@LocalStorageProp 是 ArkUI 把 formBindingData 里的字段反应式绑定到卡片 UI 的。字段名要和 FormBindingData 的 JSON key 对齐。

formProvider.updateForm 推新数据过来时,@LocalStorageProp 自动触发 UI 重渲染------卡片内容无缝刷新。

踩坑清单

  • onAddForm 必须同步返回 FormBindingData,不能 async
  • 标准解法:同步返回默认数据 + 异步 updateFormAsync + formProvider.updateForm
  • 卡片的 "占位态"直接设计成"真实的空状态"(空日期、progress=0),不要搞 Loading UI(卡片尺寸不够)
  • formId 从 want.parameters?.['ohos.extra.param.key.form_identity'] 提取,可选链 + as string 转
  • updateFormAsync 用 try/catch 吞错,失败也不致命(系统有 30 分钟 update cycle 兜底)
  • onAddForm / onUpdateForm 共享同一个 updateFormAsync 函数
  • 卡片 UI 用 @LocalStorageProp 绑定 FormBindingData 的字段,字段名要对齐 JSON key

尾声

这个 pattern 的本质是同步/异步接口的桥接。鸿蒙的卡片系统设计时考虑的是"系统桌面要立刻有内容显示",所以逼着应用必须同步返回数据。但真实应用的数据往往是异步的(在数据库/网络里)。

类似矛盾在初始化场景里也常见:

  • UIAbility 的 onCreate 要同步 OK,但真实数据加载是异步的
  • @Entry 的 build() 要同步出一个 UI,但真实数据从 Store 异步拉

万能解法 :先给占位态同步返回,然后异步触发更新。接受"卡片有半秒钟是空的"这个妥协,比强行让系统等异步要务实得多。

相关推荐
deli00720 分钟前
手机夜景模式为什么要举 3 秒?8 帧 RAW 堆栈 SNR 实测
前端
松仔log31 分钟前
Kotlin中级——callbackFlow
java·前端·kotlin
三小河31 分钟前
SWC 凭什么比 Babel 快 ?项目优化迁移踩坑记录
前端·后端
再吃一根胡萝卜1 小时前
不改一行官方源码:用一个插件把 DeepSeek Harness 换成「平安one」
前端
涛涛ing1 小时前
2026年,你选的不再是框架,是渲染模型
前端
AiMan1 小时前
Perplexity还是不可导出左侧所有对话,只能单条内容,方法不对吗
前端
AiMan1 小时前
千问生成的内容能否下载吗?
前端
北城笑笑1 小时前
Vue 109 ,从零搭建 Vue3 + Electron 工程:生态、安全桥接与打包选型( Vue + Electron )
前端·javascript·vue.js·安全·electron·vue
JarvanMo2 小时前
Flutter 3.50 & Dart 3.14发布,一起看看有哪些变化!
前端