卡片状态驱动刷新——让不同卡片实例显示不同内容的正确方式

文章目录

用户在卡片上勾了两个复选框,分别代表"状态A"和"状态B"。我想让:只有勾选了状态A,才刷新状态A的内容;只有勾选了状态B,才刷新状态B的内容。这就是"状态驱动刷新"。
StageServiceWidgetCardswidgetupdatebystatus 模块完整演示了这个功能,把它拆开讲清楚。

核心思路

卡片 UI 本身是无状态的(每次重建都从 LocalStorage 读初始值),但可以通过 message 事件把用户的选择发给 FormAbility,FormAbility 把这个"选择"存起来,然后在 onUpdateForm 里根据存储的选择决定刷新哪部分数据。

复制代码
用户勾选 → postCardAction(message, {selectA: true}) 
→ onFormEvent() 存储选择状态 
→ 下次 onUpdateForm() 读取选择状态
→ 只刷新被选中的那部分数据

卡片 UI:发送选择状态

typescript 复制代码
// entry/src/main/ets/widgetupdatebystatus/pages/WidgetUpdateByStatusCard.ets
let storageUpdateByStatus = new LocalStorage();

@Entry(storageUpdateByStatus)
@Component
struct WidgetUpdateByStatusCard {
  // 这两个值由 FormAbility 通过 updateForm 推送进来
  @LocalStorageProp('textA') textA: Resource = $r('app.string.to_be_refreshed');
  @LocalStorageProp('textB') textB: Resource = $r('app.string.to_be_refreshed');

  // 这两个是卡片内部的 UI 状态,不需要和 FormAbility 同步
  @State selectA: boolean = false;
  @State selectB: boolean = false;

  build() {
    Column() {
      // Checkbox 区域
      Column() {
        // 复选框 A
        Row() {
          Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
            .padding(0)
            .select(false)
            .margin({ left: 26 })
            .onChange((value: boolean) => {
              this.selectA = value;
              // 把选中状态发给 FormAbility
              postCardAction(this, {
                action: 'message',
                params: {
                  selectA: JSON.stringify(value)  // 注意:需要转成字符串
                }
              });
            })
          Text($r('app.string.status_a'))
            .fontColor('#000000').opacity(0.9).fontSize(14)
            .margin({ left: 8 })
        }
        .width('100%').padding(0).justifyContent(FlexAlign.Start)

        // 复选框 B
        Row() {
          Checkbox({ name: 'checkbox2', group: 'checkboxGroup' })
            .padding(0)
            .select(false)
            .margin({ left: 26 })
            .onChange((value: boolean) => {
              this.selectB = value;
              postCardAction(this, {
                action: 'message',
                params: {
                  selectB: JSON.stringify(value)
                }
              });
            })
          Text($r('app.string.status_b'))
            .fontColor('#000000').opacity(0.9).fontSize(14)
            .margin({ left: 8 })
        }
        .width('100%').position({ y: 32 }).padding(0).justifyContent(FlexAlign.Start)
      }
      .position({ y: 12 })

      // 数据显示区域
      Column() {
        Row() {
          Text($r('app.string.status_a')).fontColor('#000000').opacity(0.4).fontSize(12)
          Text(this.textA).fontColor('#000000').opacity(0.4).fontSize(12)
        }
        .margin({ top: '12px', left: 26, right: '26px' })

        Row() {
          Text($r('app.string.status_b')).fontColor('#000000').opacity(0.4).fontSize(12)
          Text(this.textB).fontColor('#000000').opacity(0.4).fontSize(12)
        }
        .margin({ top: '12px', bottom: '21px', left: 26, right: '26px' })
      }
      .margin({ top: 80 }).width('100%').alignItems(HorizontalAlign.Start)
    }
    .width('100%').height('100%')
    .backgroundImage($r('app.media.CardUpdateByStatus'))
    .backgroundImageSize(ImageSize.Cover)
  }
}

FormAbility:存储状态 + 按状态刷新

typescript 复制代码
// 对应的 FormExtensionAbility
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'WidgetUpdateByStatusFormAbility';
const DOMAIN_NUMBER = 0xFF00;
const PREF_PATH = '/data/storage/el2/base/haps/status_store';

export default class WidgetUpdateByStatusFormAbility extends FormExtensionAbility {

  onAddForm(want: Want): formBindingData.FormBindingData {
    const formData: Record<string, string> = {};
    return formBindingData.createFormBindingData(formData);
  }

  onFormEvent(formId: string, message: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, `onFormEvent: ${message}`);
    const msg: Record<string, string> = JSON.parse(message);

    // 把用户的选择状态存到 Preferences
    // key 格式:formId + '_selectA' / formId + '_selectB'
    preferences.getPreferences(this.context, PREF_PATH).then(async (prefs) => {
      if (msg.selectA !== undefined) {
        await prefs.put(`${formId}_selectA`, msg.selectA);
        hilog.info(DOMAIN_NUMBER, TAG, `存储状态A: ${msg.selectA}`);
      }
      if (msg.selectB !== undefined) {
        await prefs.put(`${formId}_selectB`, msg.selectB);
        hilog.info(DOMAIN_NUMBER, TAG, `存储状态B: ${msg.selectB}`);
      }
      await prefs.flush();
    });
  }

  onUpdateForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, `onUpdateForm: ${formId}`);

    // 读取这个 formId 的选择状态
    preferences.getPreferences(this.context, PREF_PATH).then(async (prefs) => {
      const selectAStr = await prefs.get(`${formId}_selectA`, 'false') as string;
      const selectBStr = await prefs.get(`${formId}_selectB`, 'false') as string;

      const selectA = JSON.parse(selectAStr) as boolean;
      const selectB = JSON.parse(selectBStr) as boolean;

      // 根据选择状态,决定刷新哪部分数据
      const formData: Record<string, string> = {};

      if (selectA) {
        // 只有勾选了状态A,才更新 textA
        formData['textA'] = 'AAA - 状态A已刷新';
        hilog.info(DOMAIN_NUMBER, TAG, '刷新状态A');
      }

      if (selectB) {
        // 只有勾选了状态B,才更新 textB
        formData['textB'] = 'BBB - 状态B已刷新';
        hilog.info(DOMAIN_NUMBER, TAG, '刷新状态B');
      }

      // 只有有数据需要更新时才调 updateForm
      if (Object.keys(formData).length > 0) {
        await formProvider.updateForm(
          formId,
          formBindingData.createFormBindingData(formData)
        ).catch((error: BusinessError) => {
          hilog.error(DOMAIN_NUMBER, TAG, `updateForm 失败: ${JSON.stringify(error)}`);
        });
      } else {
        hilog.info(DOMAIN_NUMBER, TAG, '无状态被选中,跳过刷新');
      }
    });
  }

  onRemoveForm(formId: string): void {
    // 清理这个 formId 的状态数据
    preferences.getPreferences(this.context, PREF_PATH).then(async (prefs) => {
      await prefs.delete(`${formId}_selectA`);
      await prefs.delete(`${formId}_selectB`);
      await prefs.flush();
    });
  }
}

进阶:onChangeFormVisibility 可见性驱动刷新

HarmonyOS 还提供了另一个维度的状态驱动:卡片可见性变化。当桌面滑动导致卡片从不可见变成可见时,触发这个回调:

typescript 复制代码
// FormExtensionAbility 里新增这个回调
onChangeFormVisibility(newStatus: Record<string, number>): void {
  hilog.info(DOMAIN_NUMBER, TAG, `可见性变化: ${JSON.stringify(newStatus)}`);

  // newStatus 的格式:{ formId: visibilityStatus }
  // visibilityStatus: 0 = 不可见, 1 = 可见
  for (const formId in newStatus) {
    const status = newStatus[formId];
    if (status === 1) {
      // 卡片变为可见,立即刷新数据
      this.onUpdateForm(formId);
    }
  }
}

这样的效果是:用户滑到卡片所在的桌面时,卡片自动触发一次刷新,显示最新数据。

状态驱动刷新完整时序图

这种方案的优点和局限

优点

  • 精准刷新,只更新需要更新的部分,省流量省计算
  • 用户自主控制,选什么刷什么,体验好
  • 数据持久化后重启手机也不会丢失选择状态

局限

  • onChangeFormVisibility 不是所有设备都支持,老版本系统可能没有
  • FormAbility 存活时间只有 5 秒,Preferences 读写要用 async/await 确保完成
  • 多个卡片实例时,要用 formId 作为 Preferences 的 key 前缀区分,不能共用同一个 key

写在最后

状态驱动刷新是一个"把控制权还给用户"的设计思路。不是所有数据都要无脑全量刷新,让用户选择感兴趣的内容,然后只刷他选的,这才是好的卡片体验。

WidgetUpdateByStatus 这个 demo 用 Checkbox 做演示,实际开发中可以换成任何有选择逻辑的 UI 元素:Tab 切换、开关、单选按钮都一样。

相关推荐
大师兄66684 天前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
大师兄66686 天前
HarmonyOS 服务卡片开发之JS 卡片开发
javascript·华为·harmonyos·harmonyos6·formkit
大师兄66689 天前
卡片数据不丢失:Preferences 在 FormKit 里的正确用法
持久化存储·preferences·harmonyos6·formkit·formid管理
大师兄666810 天前
HarmonyOS 卡片 UI 三种玩法:普通卡片、动效卡片、Canvas 卡片
harmonyos·arkts·formkit·动效卡片·canvas卡片
全栈若城19 天前
HarmonyOS Pen Kit 实战:手写笔轻捏、双击与取色器完整集成
华为·harmonyos·手写笔·harmonyos6
是稻香啊19 天前
HarmonyOS6 ArkTS TimePicker 组件使用文档
harmonyos6
全栈若城23 天前
自定义 TabBar 实战:浮动标签栏与舵式标签栏
harmonyos·harmonyos6·三方库开发
是稻香啊23 天前
HarmonyOS6 ArkTS Tabs页签超出TabBar区域显示
harmonyos6
全栈若城1 个月前
HarmonyOS6 半年磨一剑 - RcInput 组件核心架构与类型系统设计
架构·harmonyos6·三方库开发实战·rchoui·三方库开发