欢迎光临
我们一直在努力

【化学实验室|12】HarmonyOS ArkTS 桌面学习卡片实战:用 FormExtensionAbility 展示每日学习入口

桌面学习卡片的价值不是把应用首页缩小后放到桌面,而是把“今天学什么”和“点一下去哪里”前置。用户不需要先打开应用、切换学习标签、找到每日一题,再判断是否完成;卡片应该直接给出当日任务,并把一次点击送到对应页面。

本文基于 化学实验室 1.0.5 的真实 HarmonyOS ArkTS 源码,拆解 DailyStudyFormAbility.ets、DailyStudyCard.ets、form_config.json、module.json5 和 EntryAbility.ets。当前工程已经声明 FormExtensionAbility,支持 2*2 与 2*4,配置了 08:00 刷新,并用 FormLink 携带 source: 'daily_study_card' 拉起 EntryAbility。表面链路完整,继续沿数据和路由逐步检查,却能发现三个关键断点。

第一,扩展能力通过 buildFormData() 创建了标题、任务和提示,但卡片页面把同样的文案全部写死,没有读取绑定数据,因此 onUpdateForm() 即使成功也看不出任何变化。第二,更新回调每次返回的仍是同一组静态字符串,08:00 或周期更新都不会产生“今日”差异。第三,FormLink 传入的 source 没有在 EntryAbility 的冷启动或热启动生命周期中被消费,应用最终仍加载 pages/Index 的首页标签。

本文的核心判断是:桌面卡片不是一张会跳转的海报,而是一条由数据契约、刷新时机、卡片快照和启动路由共同组成的服务直达链路。 只实现其中一个环节,用户就会看到“刷新了但没变化”或“点击了却还要自己找入口”。

本文唯一校验标记:CHEM-LAB-ONE12-DAILY-STUDY-FORM-CLOSURE-20260730

每日学习卡片数据与服务直达闭环封面

本文将完成这些工程问题的拆解:

  • 还原当前卡片从配置发现到系统更新的真实链路。
  • 让 FormBindingData 成为卡片页面的唯一数据来源。
  • 用日期键、任务 ID、完成态和目标路由构造稳定契约。
  • 设计冷启动与热启动都能消费的卡片跳转参数。
  • 区分 2*2 与 2*4 的信息密度,而不是简单拉伸。
  • 明确刷新策略、失败回退、实例清理和日志边界。
  • 给出可复现的真机测试矩阵与故障定位顺序。

一、先理解卡片提供方与卡片使用方

在当前场景中,化学实验室是卡片提供方,系统桌面是卡片使用方。应用负责声明卡片、生成数据、实现 UI 和处理交互;桌面负责添加、展示、删除和触发系统调度。

官方 Form Kit 术语将静态 ArkTS 卡片描述为适合 UI 相对固定、交互简单的场景:卡片渲染完成后,宿主使用最后一帧快照并释放运行资源。对于“每天展示一个学习任务、点击进入应用”的需求,这种形态是合理的,没有必要为了一个入口升级为长期运行的复杂动态卡片。

这里的“静态”不等于“永远不更新”。提供方仍可以在添加和更新回调中生成新的绑定数据,再由系统刷新卡片快照。

二、当前工程的四个声明位置

卡片能被系统发现,至少依赖四处一致配置:

  • module.json5 声明类型为 form 的 DailyStudyFormAbility。
  • 扩展能力通过 metadata 引用 $profile:form_config。
  • form_config.json 声明 DailyStudyCard 的页面、尺寸和更新配置。
  • src 指向 ./ets/widget/pages/DailyStudyCard.ets。
  • 当前 module.json5 的关键配置为:

    "extensionAbilities": [
    {
    "name": "DailyStudyFormAbility",
    "srcEntry": "./ets/formability/DailyStudyFormAbility.ets",
    "label": "$string:daily_study_form_label",
    "description": "$string:daily_study_form_desc",
    "type": "form",
    "metadata": [
    {
    "name": "ohos.extension.form",
    "resource": "$profile:form_config"
    }
    ]
    }
    ]

    这一层已经正确建立了“系统配置 -> 扩展能力 -> 卡片描述文件”的引用关系。后续排障时,只有系统完全看不到卡片,才优先检查这些声明;如果卡片能添加但内容不更新,应转向数据绑定链路。

    三、form_config.json 决定系统怎样看待卡片

    当前配置支持 2*2 和 2*4,默认尺寸是 2*2:

    {
    "name": "DailyStudyCard",
    "src": "./ets/widget/pages/DailyStudyCard.ets",
    "uiSyntax": "arkts",
    "colorMode": "auto",
    "updateEnabled": true,
    "scheduledUpdateTime": "08:00",
    "updateDuration": 1,
    "defaultDimension": "2*2",
    "supportDimensions": ["2*2", "2*4"]
    }

    这里应重点验证三件事。第一,支持尺寸必须有真实布局适配。第二,colorMode: auto 只有在页面使用可随模式变化的资源时才有意义。第三,定点和周期更新字段应根据项目目标 SDK 的官方说明选用并在真机验证,不应只因为模板生成了两个字段就同时保留。

    每日学习任务通常不需要高频刷新。若内容只在日期变化或用户完成任务后改变,可以采用“每日定点 + 业务事件主动更新”的组合,避免无意义周期唤醒。

    四、当前扩展能力的生命周期实现

    DailyStudyFormAbility 已覆盖创建、更新和删除:

    export default class DailyStudyFormAbility extends FormExtensionAbility {
    onAddForm(want: Want): formBindingData.FormBindingData {
    return buildFormData()
    }

    onUpdateForm(formId: string): void {
    formProvider.updateForm(formId, buildFormData()).catch((err: Error) => {
    hilog.error(DOMAIN, 'DailyStudyForm',
    'update form failed: %{public}s', err.message)
    })
    }

    onRemoveForm(formId: string): void {
    hilog.info(DOMAIN, 'DailyStudyForm', 'onRemoveForm: %{public}s', formId)
    }
    }

    生命周期框架本身没有缺失。真正的问题是 buildFormData() 返回什么、卡片页面是否读取它,以及点击后应用是否消费路由参数。

    五、数据提供方与卡片 UI 目前各写了一份文案

    扩展能力构造了:

    interface DailyStudyFormData {
    title: string
    subtitle: string
    task: string
    tip: string
    }

    但 DailyStudyCard.ets 又直接写了“今日化学学习”“完成一道化学题”等同样文本。两份内容暂时一样,容易让开发者误以为绑定成功;只要把 task 改成另一个值再更新,就会发现卡片没有变化。

    这是一类典型的“假闭环”:提供方生成了数据,更新 API 也返回成功,但渲染页根本没有消费。判断刷新是否有效,必须以桌面实际快照变化为准,不能只看 updateForm() Promise 成功。

    六、把绑定字段接入 ArkTS 卡片页面

    卡片页面应为绑定字段提供状态入口,默认值只用于数据尚未就绪时的可控展示:

    @Entry
    @Component
    struct DailyStudyCard {
    @StorageProp('title') title: string = '今日化学学习'
    @StorageProp('subtitle') subtitle: string = '准备今天的学习任务'
    @StorageProp('taskTitle') taskTitle: string = '打开应用查看任务'
    @StorageProp('statusText') statusText: string = '待开始'
    @StorageProp('targetRoute') targetRoute: string = 'daily_quiz'

    build() {
    // Text(this.title)、Text(this.taskTitle) 等从绑定字段渲染
    }
    }

    字段名称必须与 createFormBindingData() 中的对象键完全一致。默认值不承担每日业务逻辑,只保证异常情况下卡片不出现空白。

    七、重新定义每日学习卡片的数据契约

    仅有四段展示文本不足以支持去重、更新、跳转和完成态。建议把数据契约扩展为:

    export interface DailyStudyFormData {
    schemaVersion: number
    dateKey: string
    taskId: string
    taskType: 'quiz' | 'reading' | 'experiment_review'
    title: string
    subtitle: string
    taskTitle: string
    statusText: string
    completed: boolean
    targetRoute: string
    }

    schemaVersion 用于未来兼容字段变化,dateKey 判断是否跨日,taskId 保证内容身份稳定,taskType 决定图标和目标页面,completed 决定视觉状态。卡片 UI 不再根据中文文案猜业务状态。

    八、数据生成不能散落在 FormExtensionAbility 中

    扩展能力应管理生命周期,业务数据由仓库或服务提供:

    export interface DailyStudyTask {
    id: string
    dateKey: string
    type: 'quiz' | 'reading' | 'experiment_review'
    title: string
    completed: boolean
    targetRoute: string
    }

    export class DailyStudyRepository {
    async getTask(dateKey: string): Promise<DailyStudyTask> {
    // 从项目现有本地数据服务读取,失败时返回受控默认任务
    return this.buildFallbackTask(dateKey)
    }
    }

    如果卡片数据来自本地持久化,仓库应使用扩展能力可用的上下文初始化存储。不要让卡片页面直接访问应用页面的内存状态,因为卡片渲染与应用 UI 不是同一个页面生命周期。

    九、创建卡片时要记录实例维度

    一个用户可能同时添加多个卡片实例,且尺寸不同。onAddForm(want) 中的参数可以用于识别实例与维度;解析时应做类型保护,不要直接假设字段一定存在:

    interface FormRequestInfo {
    formId: string
    dimension: number
    }

    function readFormRequest(want: Want): FormRequestInfo {
    const params = want.parameters
    const formId = String(params?.['ohos.extra.param.key.form_identity'] ?? '')
    const rawDimension = Number(params?.['ohos.extra.param.key.form_dimension'] ?? 0)
    return {
    formId,
    dimension: Number.isFinite(rawDimension) ? rawDimension : 0
    }
    }

    具体系统参数键和可用 API 应以项目目标 SDK 的 Form Kit 文档为准。示例的重点是:系统输入先规范化,再进入业务逻辑;日志和持久化只保存确实需要的字段。

    十、不要把完整 want.parameters 作为公开日志

    当前代码把 JSON.stringify(want.parameters) 以 %{public}s 输出。现阶段参数主要是卡片元数据,但未来一旦加入用户选择、任务 ID 或来源信息,完整公开日志会扩大泄露范围。

    更稳的日志只记录排障所需字段:

    hilog.info(
    DOMAIN,
    'DailyStudyForm',
    'add form, id=%{public}s, dimension=%{public}d',
    request.formId,
    request.dimension
    )

    任务标题、学习记录和用户内容不应写入公开日志。错误日志记录错误码与受控消息,不输出整个对象。

    十一、构造数据时要让日期和任务真正变化

    可把“当前日期 -> 今日任务 -> 卡片契约”集中在一个构造器中:

    function toFormData(task: DailyStudyTask): DailyStudyFormData {
    return {
    schemaVersion: 1,
    dateKey: task.dateKey,
    taskId: task.id,
    taskType: task.type,
    title: '今日化学学习',
    subtitle: task.completed ? '今日任务已完成' : '每天前进一小步',
    taskTitle: task.title,
    statusText: task.completed ? '已完成' : '待完成',
    completed: task.completed,
    targetRoute: task.targetRoute
    }
    }

    同一天的同一个任务应生成稳定 taskId,避免每次更新都变成新任务;跨日后 dateKey 和任务身份才更新。

    十二、更新流程要有加载失败回退

    onUpdateForm() 返回 void,内部可以启动异步更新,但必须在 Promise 链中收口错误:

    onUpdateForm(formId: string): void {
    this.refreshForm(formId).catch((error: Error) => {
    hilog.error(
    DOMAIN,
    'DailyStudyForm',
    'refresh failed, id=%{public}s, reason=%{public}s',
    formId,
    error.message
    )
    })
    }

    private async refreshForm(formId: string): Promise<void> {
    const dateKey = this.clock.todayKey()
    const task = await this.repository.getTask(dateKey)
    const binding = formBindingData.createFormBindingData(toFormData(task))
    await formProvider.updateForm(formId, binding)
    }

    这里把时钟也抽成依赖,是为了在测试中固定日期,验证跨日更新而不等待第二天。

    每日学习卡片从创建、取数、刷新到服务直达的运行流程

    十三、每日卡片应该选择什么刷新策略

    当前配置同时存在 scheduledUpdateTime: '08:00' 与 updateDuration: 1。不同 API 版本对更新字段的约束和调度行为需要按目标 SDK 官方文档确认,工程上不应依赖未验证的组合。

    对于化学学习卡片,可以定义清楚的业务策略:

  • 每天早上刷新当天任务。
  • 用户完成题目或阅读后主动更新完成态。
  • 应用升级或数据版本变化时重建契约。
  • 无需为了静态任务每隔短时间重复刷新。
  • 系统调度不是精确定时器,测试时应允许延迟;业务正确性不能依赖“08:00 整秒一定回调”。

    十四、完成任务后如何主动刷新卡片

    当用户在应用内完成每日一题,业务层应先保存完成记录,再通知卡片更新。不要让答题页直接拼装卡片字段,可以调用统一服务:

    export class DailyStudyCardUpdater {
    async refresh(formIds: string[]): Promise<void> {
    const task = await this.repository.getTask(this.clock.todayKey())
    const data = formBindingData.createFormBindingData(toFormData(task))
    for (const formId of formIds) {
    await formProvider.updateForm(formId, data)
    }
    }
    }

    如果项目不需要保存实例 ID,就采用 Form Kit 针对当前版本提供的查询或更新方案;若保存实例 ID,必须在删除回调中清理失效记录。

    十五、onRemoveForm 是否需要清理取决于是否保存实例状态

    当前 onRemoveForm() 只记录日志。在完全无状态的卡片中,这足够;一旦为不同卡片保存尺寸、任务偏好或实例 ID,就必须删除对应数据:

    onRemoveForm(formId: string): void {
    this.formInstanceRepository.remove(formId).catch((error: Error) => {
    hilog.error(DOMAIN, 'DailyStudyForm',
    'remove instance failed: %{public}s', error.message)
    })
    }

    删除回调应幂等:记录已经不存在时也视为成功,避免系统重复通知造成异常。

    十六、当前 FormLink 只证明能拉起应用

    卡片页面把整个内容包在 FormLink 中:

    FormLink({
    action: 'router',
    abilityName: 'EntryAbility',
    params: {
    source: 'daily_study_card'
    }
    }) {
    // card content
    }

    它能够携带来源参数拉起 EntryAbility,但“拉起应用”和“直达学习任务”不是同一件事。当前 EntryAbility.onCreate() 没有读取 want.parameters,也没有 onNewWant();随后固定加载 pages/Index。卡片点击因此只完成了链路前半段。

    十七、为跳转定义稳定参数,不能只传 source

    建议传递最小、可验证的启动契约:

    export interface DailyStudyLaunchParams {
    source: 'daily_study_card'
    target: 'daily_quiz' | 'reading' | 'experiment_review'
    taskId: string
    dateKey: string
    }

    source 用于统计和回退,target 决定页面,taskId 定位内容,dateKey 防止用户点击昨天的快照后误打开今天的另一条任务。卡片不传完整题目正文,避免参数过大和数据过期。

    十八、冷启动与热启动必须分别处理

    应用未运行时,参数从 onCreate(want, …) 进入;应用已在前台或后台时,新的启动请求可能进入 onNewWant(want, …)。两条生命周期都应调用同一个规范化函数:

    private acceptLaunchWant(want: Want): void {
    const launch = DailyStudyLaunchParser.parse(want.parameters)
    if (launch === undefined) {
    return
    }
    AppStorage.setOrCreate<DailyStudyLaunchParams>('pendingDailyLaunch', launch)
    }

    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.acceptLaunchWant(want)
    // 其余初始化
    }

    onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.acceptLaunchWant(want)
    }

    具体生命周期签名以项目当前 SDK 类型定义为准。核心是冷、热启动共享解析逻辑,避免一种场景能直达、另一种只回首页。

    十九、页面加载与数据初始化存在竞态

    当前 EntryAbility.onCreate() 异步执行 DataStore.init(),onWindowStageCreate() 直接加载 pages/Index。如果卡片目标页依赖存储,路由消费可能早于数据初始化完成。

    可以让启动协调器保存两个条件:页面已就绪、数据已就绪。只有两者都满足时才消费 pendingDailyLaunch:

    interface StartupState {
    contentReady: boolean
    dataReady: boolean
    }

    private tryDispatchPendingLaunch(): void {
    if (!this.startupState.contentReady || !this.startupState.dataReady) return
    this.launchCoordinator.dispatchPending()
    }

    如果任务读取失败,回退到学习首页并显示可恢复提示,不能留下白屏或无响应点击。

    二十、Index 页需要接住学习标签与二级页面目标

    当前 Index.ets 的 currentIndex 默认为 0,学习标签是索引 2。最小改造是先让启动协调器切换到学习标签,再由页面路由进入 DailyQuizPage:

    @StorageLink('requestedTabIndex') requestedTabIndex: number = 0

    aboutToAppear(): void {
    this.currentIndex = this.requestedTabIndex
    this.tabController.changeIndex(this.requestedTabIndex)
    }

    二级页面跳转应继续走项目已有路由方式。若路由失败或目标任务已过期,停留在学习标签,而不是返回空白页。

    二十一、旧卡片快照必须有过期处理

    用户可能在第二天 08:00 刷新前点击昨天卡片。应用收到 dateKey 后应与当前日期比较:

    if (launch.dateKey !== this.clock.todayKey()) {
    return {
    target: 'learning_home',
    message: '卡片内容已更新,请查看今天的学习任务'
    }
    }

    这比盲目使用 taskId 更安全。页面可以随后触发一次卡片刷新,让桌面快照尽快与当天内容一致。

    二十二、2×2 与 2×4 不应该共用同一信息密度

    当前配置声明两个尺寸,但只有一个卡片页面布局。它可以被拉伸,却没有利用 2*4 的额外空间。建议保持同一数据契约,按尺寸设计两种信息层级:

    尺寸必须显示可选显示
    2×2 标题、今日任务、完成态、打开按钮 一行简短副标题
    2×4 2×2 全部内容 学习连续天数、预计用时、下一任务

    尺寸适配不是把字号放大。2×2 要保证最长任务名省略后仍能看懂,2×4 则利用宽度提供一个额外决策信息点。

    二十三、卡片 UI 要从硬编码颜色迁移到资源

    DailyStudyCard.ets 直接使用 #0052D9、#047481、#FFFFFF 等颜色,同时配置了 colorMode: auto。如果没有深浅色资源映射,“auto”不会自动让硬编码渐变和文字达到合理对比。

    卡片资源至少应区分:背景起止色、主文字、次文字、状态标签、按钮背景和完成态。文字在渐变最亮和最暗位置都要可读;系统深色模式、不同壁纸和桌面缩放下都需要截图验证。

    二十四、点击区域要明确且不制造误触

    当前整个卡片都是 FormLink,适合只有一个动作的静态入口。但卡片内部又绘制了“学习”和“打开”两个视觉按钮,用户可能误以为它们有不同功能。

    如果卡片只有一个目标,保留整卡点击,同时把视觉按钮统一成一个明确动作;如果 2×4 未来出现多个入口,应分别定义交互区域,并确保参数目标不同。交互设计不能比实际能力更复杂。

    二十五、卡片数据的空态、错误态和完成态

    卡片没有普通页面那么多空间,但仍需要三类受控状态:

    export type CardContentState = 'ready' | 'completed' | 'fallback'

    • ready:显示任务与“去完成”。
    • completed:显示已完成和次日提示,不重复催促。
    • fallback:数据读取失败时显示“打开应用继续学习”,仍保留安全入口。

    不要在卡片上展示技术错误码。错误码进入日志,用户看到的是可执行的回退动作。

    二十六、职责拆分后的整体结构

    推荐的依赖方向是:

    FormExtensionAbility
    -> DailyStudyCardService
    -> DailyStudyRepository
    -> FormDataMapper
    -> FormProviderAdapter

    DailyStudyCard UI
    -> FormBindingData fields
    -> FormLink launch contract

    EntryAbility
    -> LaunchParser
    -> StartupCoordinator
    -> Index / target page

    扩展能力不拼业务文案,卡片 UI 不查持久化,入口 Ability 不理解题目数据。每一层只处理一种变化原因。

    FormExtensionAbility、任务仓库、卡片页面和启动路由的职责结构

    二十七、对构造器做纯函数测试

    toFormData() 不依赖系统上下文,最适合先写单元测试:

    const task: DailyStudyTask = {
    id: 'quiz-20260717',
    dateKey: '2026-07-17',
    type: 'quiz',
    title: '完成一道酸碱平衡题',
    completed: false,
    targetRoute: 'daily_quiz'
    }

    const data = toFormData(task)

    expect(data.taskId).assertEqual('quiz-20260717')
    expect(data.statusText).assertEqual('待完成')
    expect(data.targetRoute).assertEqual('daily_quiz')

    还要验证完成态、空标题回退、超长标题截断策略和未知任务类型。纯函数测试失败时,不必启动桌面卡片环境。

    二十八、用适配器测试更新失败

    直接调用系统 formProvider 不易制造异常,可以封装窄接口:

    export interface FormUpdater {
    update(formId: string, data: DailyStudyFormData): Promise<void>
    }

    export class SystemFormUpdater implements FormUpdater {
    async update(formId: string, data: DailyStudyFormData): Promise<void> {
    const binding = formBindingData.createFormBindingData(data)
    await formProvider.updateForm(formId, binding)
    }
    }

    测试替身可以模拟实例不存在、数据加载失败和连续两次更新。服务层应记录失败但不崩溃,也不能把一个实例失败误判成所有实例失败。

    二十九、冷启动与热启动测试矩阵

    场景前置状态预期结果
    冷启动点击 应用未运行 初始化完成后进入每日一题
    热启动点击 应用停在实验室标签 切换学习标签并进入目标页
    旧日期点击 卡片 dateKey 为昨天 回退学习首页并提示内容更新
    未知 target 参数被篡改或版本过旧 忽略目标,安全进入学习首页
    数据初始化失败 本地仓库不可用 不白屏,展示可重试提示
    重复点击 连续点击同一卡片 不叠加重复目标页

    如果只验证冷启动,应用已在运行时的 onNewWant 漏洞很容易带到发布版本。

    三十、卡片显示测试矩阵

    检查项2×22×4
    普通任务 标题与动作完整 增加辅助信息
    超长任务 单行省略且主动作可见 最多两行,不挤压按钮
    完成态 明确“已完成” 展示下一步建议
    数据失败 回退文案和入口 回退文案和入口
    深色模式 文字、渐变、状态标签可读 同左
    大字体 不重叠、不截断核心动作 信息按优先级收缩

    还要在手机、平板和 2in1 的桌面环境中验证实际卡片支持情况。应用声明支持这些设备,不代表桌面卡片布局自动适配完成。

    三十一、常见故障一:系统完全找不到卡片

    按下面顺序检查:

  • module.json5 是否声明 type: 'form'。
  • srcEntry 路径与类文件是否一致。
  • metadata 是否引用正确 profile。
  • form_config.json 的 name、src 和 uiSyntax 是否有效。
  • 卡片页面是否成功编译进包。
  • 安装的是不是包含最新配置的构建产物。
  • 此时不要先改业务数据,因为系统尚未走到数据阶段。

    三十二、常见故障二:updateForm 成功但界面不变

    先把绑定字段临时改成可辨识值,例如标题追加日期。若桌面仍不变,检查:

  • 卡片页面是否用 @StorageProp 或当前 SDK 对应绑定方式读取同名键。
  • createFormBindingData() 是否包含该字段。
  • 更新的 formId 是否对应桌面上的实例。
  • 桌面宿主是否已接收新快照。
  • 是否实际上仍在看旧的另一个卡片实例。
  • 当前项目首先应修复第 1 项,因为卡片页面仍使用硬编码文本。

    三十三、常见故障三:点击卡片只回首页

    确认 FormLink 的 abilityName 能匹配 EntryAbility 后,继续检查:

  • onCreate 是否读取启动参数。
  • 热启动时是否实现 onNewWant。
  • 参数是否在页面加载前被覆盖。
  • Index 是否消费标签切换请求。
  • 目标页路由是否在 main_pages.json 中存在。
  • 当前 DailyQuizPage 已在 main_pages.json 中,主要缺口是参数接收和启动协调。

    三十四、常见故障四:08:00 没有准时刷新

    系统卡片更新受版本、调度策略、设备状态和配置约束影响,不应当作精确闹钟。排查时记录实际回调时间,确认 updateEnabled 与目标 SDK 配置有效,并在业务完成事件后主动更新。

    若每日内容必须在首次查看时正确,卡片点击进入应用后还应重新校验当前日期,不能把正确性完全押在系统定点刷新上。

    三十五、真机验收清单

  • 安装新包后能在桌面卡片列表找到“今日化学学习”。
  • 添加 2×2 与 2×4 两种实例,布局均无重叠。
  • 首次添加时绑定数据与当日任务一致。
  • 修改今日任务并触发更新后,桌面文案发生变化。
  • 完成任务后卡片变成已完成态。
  • 冷启动点击直达对应学习页。
  • 应用已运行时再次点击仍能直达。
  • 删除一个实例后,其实例记录被清理且其他实例不受影响。
  • 重启设备后卡片不白屏,点击仍有安全回退。
  • 浅色、深色、大字体和不同壁纸下均可读。
  • 三十六、分阶段落地更容易定位问题

    第一阶段只把 FormBindingData 接入卡片页面,用一个可辨识日期字段证明数据闭环。第二阶段接入每日任务仓库和完成态。第三阶段定义启动契约,分别打通冷、热启动。第四阶段完成 2×2、2×4 和主题适配。第五阶段补主动刷新、实例清理和真机回归。

    每阶段只改变一个验证点:看到卡片变化、看到任务变化、看到页面直达、看到尺寸适配、看到完成态同步。这样一旦失败,可以立刻定位到对应层。

    三十七、完成标准应该写成可观察结果

    这张卡片的验收不是“代码里有 FormExtensionAbility”,而是:

    • 系统能发现并添加卡片。
    • 卡片显示的是绑定数据,不是页面硬编码副本。
    • 日期或完成态变化后,桌面快照能被更新。
    • 点击参数在冷启动和热启动都被消费。
    • 目标无效或数据失败时,安全回退到学习首页。
    • 2×2 与 2×4 都提供清晰、不过载的信息。

    三十八、官方资料与版本核对入口

    本文结合项目真实源码与当前工程结构给出改造方案。Form Kit 配置字段、生命周期签名和交互能力会随 API 版本演进,落地时应以项目目标 SDK 的华为官方文档为准:

    • Form Kit 术语与卡片角色
    • Form Kit 产品与能力入口
    • HarmonyOS 开发文档中心

    特别是更新策略、系统参数键和 onNewWant 的准确签名,应在 DevEco Studio 当前 SDK 类型提示中再次确认,并通过真机回调日志验证。

    三十九、源码事实与建议实现的边界

    为了避免把设计方案写成已经上线的能力,下面将当前仓库可以复核的 Form Kit 实现与建议演进明确分开。

    观察维度当前源码事实建议实现
    扩展能力 DailyStudyFormAbility 继承 FormExtensionAbility,实现添加、更新和移除回调 保留系统生命周期入口,把数据构建委托给可测试的每日任务服务
    初始数据 buildFormData() 返回标题、副标题、任务和提示四个固定字符串 使用日期键、任务 ID、完成态、目标路由和版本字段构建稳定契约
    更新行为 onUpdateForm 调用 formProvider.updateForm,但每次仍生成同一组静态字符串 更新时读取真实任务状态;失败时保留最近有效快照并记录可定位日志
    卡片 UI DailyStudyCard.ets 中标题、副标题和任务文案全部硬编码 从 FormBindingData 读取字段,避免提供方数据与 UI 副本分叉
    点击行为 FormLink 使用 router 动作,目标为 EntryAbility,参数只有 source: daily_study_card 增加受控目标路由与任务 ID,并对未知目标做白名单回退
    冷启动 EntryAbility.onCreate 接收 Want,但当前没有消费卡片 source 在初始化完成后解析启动契约,并把一次性导航意图交给页面路由层
    热启动 当前 EntryAbility 没有实现卡片参数的 onNewWant 消费路径 按目标 SDK 的准确签名补热启动处理,避免应用已运行时点击失效
    首屏加载 onWindowStageCreate 固定加载 pages/Index 继续加载稳定首屏,再由明确的导航状态直达目标或安全回退
    尺寸与更新 配置支持 2*2、2*4,默认 2*2,启用更新并配置 08:00 与周期参数 为两个尺寸定义不同信息密度,并在目标 SDK 与真机上验证实际调度语义
    主题策略 卡片配置使用 colorMode: auto,页面本身采用固定渐变与白色文字 验证深浅色、壁纸、大字体与快照对比度,必要时提供资源化主题

    本文提出的每日任务仓库、版本化 DailyStudyFormData、日期键、完成态同步、尺寸分级布局、冷/热启动统一消费器和主动刷新策略,都是建议实现,当前源码尚未集成。本次精修没有执行 DevEco Studio 构建、卡片安装添加、08:00 调度、设备重启或真机点击直达测试,因此不能据此声称卡片数据闭环和服务直达已经修复;相关配置字段与生命周期签名还必须以项目目标 SDK 类型和真机日志为准。

    总结

    化学实验室现有桌面卡片已经完成了声明、扩展能力、卡片页面和 FormLink 的基础搭建,最值得修复的不是视觉细节,而是两条没有闭环的链路:FormBindingData 没有进入 UI,FormLink 参数没有进入业务页面。更新回调反复提交静态文案、两个尺寸共用同一信息密度,也让“每日学习入口”的产品价值没有真正体现。

    把每日任务建模为带日期键、任务 ID、完成态和目标路由的稳定契约,再让 Form 扩展、卡片页面和启动路由共同消费这份契约,才能得到可信的服务直达体验。卡片显示什么、何时更新、点击去哪、失败如何回退,都必须能被真机观察和测试,而不是只在配置文件里看起来完整。

    AI 辅助声明:本文由 AI 辅助完成源码梳理、结构校对与配图设计,技术结论均以文中列出的本地 ArkTS 源码为依据,并由作者复核后发布。

    赞(0)
    未经允许不得转载:171主机测评 » 【化学实验室|12】HarmonyOS ArkTS 桌面学习卡片实战:用 FormExtensionAbility 展示每日学习入口
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址