桌面学习卡片的价值不是把应用首页缩小后放到桌面,而是把“今天学什么”和“点一下去哪里”前置。用户不需要先打开应用、切换学习标签、找到每日一题,再判断是否完成;卡片应该直接给出当日任务,并把一次点击送到对应页面。
本文基于化学实验室 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 不理解题目数据。每一层只处理一种变化原因。
二十七、对构造器做纯函数测试
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×2 | 2×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 源码为依据,并由作者复核后发布。