鸿蒙应用开发之家庭应急物资分类页深度实践:@State 驱动三分类胶囊与加减库存交互机制

1、引言

场景是这样的:我们团队最近在开发一款面向家庭用户的应急物资管理应用。产品经理提出了一个极其务实的诉求——用户进入"物资分类"页面后,不仅要能按分类快速定位物资(食品饮水/照明电源/医疗急救),还要能在列表里直接加减库存数量,买了罐头点一下加号、用了电池点一下减号,一步到位。

在传统的移动端开发中,这种"分类导航 + 步进器列表"的组合布局,往往需要用 TabLayout 嵌套 RecyclerView,步进器要自定义 ViewHolder 持有按钮引用,分类切换时列表数据源的隔离与刷新逻辑极其容易写出 bug——切了分类库存数据丢了,或者加减按钮点错了行。

幸运的是,在 HarmonyOS 的 ArkUI 声明式开发范式中,@State 装饰器给了我们一种极其优雅的方案:用一个 activeCat 状态变量驱动分类切换,ForEach 自动响应数据源变化重渲染列表,步进器用三个圆形容器横向排列,整页代码不到 110 行。

这篇实战手记,我们将从 60-emergency-kit/entry/src/main/ets/pages/Func1Tab.ets(共 102 行)切入,不仅教你如何用 @State 驱动三分类胶囊的选中态切换,更重要的是,借此机会彻底拆解"减号灰圆 / 数字居中 / 加号橙圆"步进器三件套的视觉编码逻辑,以及分类切换时数据隔离的底层机制。我们不背文档,不讲空话,直接看真实场景下的解法。

2、效果展示与页面结构

在正式深入代码之前,我们先看看最终的页面表现和工程底座。直观的视觉冲击能让你瞬间明白我们在解决什么问题。

物资页是"分类导航 + 物品管理"的组合布局,整体由 3 个 @Builder 块构成:

build() {
  Column() {
    this.Header()
    this.CatRow()
    Scroll() {
      Column({ space: 10 }) {
        this.ItemList()
      }
      .width('100%')
      .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 })
    }
    .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
  }
  .width('100%').height('100%').backgroundColor(C.bg)
}

这个结构的设计初衷是明确划分"导航"和"管理"的职责边界:

  1. Header — 单行品牌头(“物资分类”),白底固定,20 号加粗,点明本页职能
  2. CatRow — 三分类等分胶囊(食品饮水/照明电源/医疗急救),选中橙底白字 + 描边
  3. ItemList — 物品列表(随分类切换),每条含图标 + 名称 + − 数量 + 步进器

物资页回答的核心问题是"物资怎么组织、库存怎么调整"——分类胶囊解决"物资怎么组织"(3 分类 8 种物品),步进器列表解决"库存怎么调整"(− 数量 + 快速加减),一页完成"看分类、调库存"。

物资页整体布局

3、Header:单行品牌头的标准范式
Header() {
  Column() {
    Text('物资分类').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text)
      .width('100%').padding({ top: this.safeTop + 10, left: D.pad, right: D.pad, bottom: 10 })
  }.width('100%').backgroundColor(C.card)
}

功能页 Header 的标准范式:20 号字号、safeTop + 10 避让状态栏、底部仅 10 给分类栏让位。与系列功能页 Header 一致——品牌一致性是系列应用的基本素养。"物资分类"点明本页职能——按分类管理应急物资。

这里有一个极其关键的细节:底部 padding 只有 10(比首页的 12 更小),因为下方紧接分类胶囊栏,压缩 Header 底部留白让分类栏更早进入用户视野——这是"功能页 Header 让位导航栏"的节奏控制。

4、CatRow:三分类等分胶囊与选中态编码
CatRow() {
  Row({ space: 10 }) {
    ForEach(this.cats, (c: Category, idx: number) => {
      Text(c.name).fontSize(13)
        .fontColor(this.activeCat === idx ? '#FFFFFF' : C.textSub)
        .textAlign(TextAlign.Center).layoutWeight(1).padding({ top: 9, bottom: 9 })
        .backgroundColor(this.activeCat === idx ? C.primary : C.card)
        .border({ width: 1, color: this.activeCat === idx ? C.primary : C.stroke })
        .onClick(() => { this.activeCat = idx; })
    }, (c: Category) => c.id.toString())
  }.width('90%').padding({ top: 10, bottom: 10 })
}

这段代码中有几处极其精妙的设计:

第一,三分类等分的布局策略。食品饮水/照明电源/医疗急救——layoutWeight(1) 三等分 + textAlign(TextAlign.Center) 居中,宽度 90% 居中(两侧留白 5%),一行排开。三个分类覆盖应急场景的核心需求:食品饮水(吃)、照明电源(亮)、医疗急救(治),"吃、亮、治"覆盖应急生存的三大维度。

第二,选中态的双重属性变化。选中时"底 + 边"同时变橙(backgroundColor primary + border primary)、未选"底白 + 边灰"(C.card + C.stroke)——双属性变化让选中态非常明确(比仅变色底更醒目)。未选的"白底灰边"与页面背景区分靠灰描边(不用灰底避免与背景糊在一起),描边式胶囊是"页面内嵌导航"的适配形态。

第三,默认选中"食品饮水"的优先级设计activeCat = 0(第一类)——食品饮水是应急物资的第一优先(水是生命必需品),默认展示最重要的一类,用户进入页面即可看到最关键的物资。

第四,@State 驱动的列表联动onClick 更新 activeCat——列表随分类切换cats[activeCat].items 动态渲染)。@State 装饰器让 activeCat 变化时自动触发 ForEach 重渲染,无需手动调用 notifyDataSetChanged——这是声明式范式相比命令式的核心优势。

5、ItemList:步进器三件套与加减库存
ItemList() {
  Column({ space: 10 }) {
    ForEach(this.cats[this.activeCat].items, (it: Item) => {
      Row({ space: 12 }) {
        Row() { Text(it.emoji).fontSize(24) }
        .width(48).height(48).backgroundColor(C.primarySoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
        Text(it.name).fontSize(14).fontColor(C.text).layoutWeight(1)
        Row({ space: 12 }) {
          Text('−').fontSize(18).fontColor(C.textSub)
            .width(28).height(28).textAlign(TextAlign.Center).backgroundColor(C.cardSoft).borderRadius(14)
          Text(it.count.toString()).fontSize(14).fontColor(C.text).width(28).textAlign(TextAlign.Center)
          Text('+').fontSize(18).fontColor('#FFFFFF')
            .width(28).height(28).textAlign(TextAlign.Center).backgroundColor(C.primary).borderRadius(14)
        }
      }
      .width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
    }, (it: Item) => it.id.toString())
  }.width('100%')
}

食品饮水分类 3 条物品(27-31 行):

物品 数量
💧 瓶装水 24
🥫 罐头 6
🍪 压缩饼干 10

这段代码中有几处极其精妙的设计:

第一,步进器的视觉编码。减号灰(textSub + cardSoft 灰底)、加号橙(白字 + primary 橙底)——“减是次要操作用灰、加是主要操作用橙”(补充物资是主场景,加比减常用且正向),颜色的强弱编码操作的主次,用户直觉"点橙色的加"(补充)而非灰色的减。若两按钮同色则主次不分。

第二,步进器三件套的圆形容器。− 按钮(18 号灰字 + 28×28 灰圆底 cardSoft + 圆角 14)+ 数量(14 号居中 28 宽)+ + 按钮(18 号白字 + 28×28 橙圆底 primary)——三个圆形容器横向排列,圆形按钮 + 居中数字的"步进器"形态(stepper),是系列中唯一的步进器交互。

第三,数量显示的位置稳定性it.count.toString()(24/6/10)——居中 28 宽固定(数字位置稳定,增减时不动),用户点击加减时只有数字变、位置不变,视觉焦点稳定。

第四,主色软底图标的品牌统一backgroundColor(C.primarySoft)#FDE8DC 浅橙)+ 24 号 emoji——浅橙圆角底,物品类型一眼可辨,与首页物资卡片同款图标块。

第五,分类切换的数据隔离cats[this.activeCat].items 直接索引——分类切换时列表重渲染(ForEach key 是 it.id 全局唯一,跨分类不冲突),食品饮水的库存(24/6/10)切到照明电源(2/12/8)再切回来数据不变,分类是"视图过滤"而非"数据清空"。

切换到照明电源分类

6、跨页数据自洽:物资分类的全局闭环

物资页不是孤岛——它与首页、检查页、我的页形成四页数据闭环:

  • 物资页"食品饮水"分类 ↔ 首页"瓶装饮用水 24 件"——物资页的瓶装水(24)就是首页的关键物资,物资页是全集、首页是关键项(同一数据)。
  • 物资页"罐头 6" ↔ 首页"罐头食品 6 件 偏少"、检查页"罐头食品 库存仅剩 6 件"——数量完全一致(6 件),物资页加减库存后首页/检查页状态联动更新(6 → 12 则偏少 → 充足)。
  • 物资页"照明电源"分类(手电筒 2/电池 12/蜡烛 8) ↔ 首页"手电筒 2 件 充足"——手电筒 2 件在物资页与首页一致,分类内的物品状态影响首页关键项
  • 物资页"医疗急救"分类(急救包 1/常用药 5) ↔ 首页"医药急救包 1 件 偏少"、检查页"医药急救包 库存仅剩 1 个"——急救包 1 件三页一致,医疗急救是最需关注的关键物资。

这套闭环的核心是统一数据源——四页共享同一份物资数据模型,物资页是"库存增改的入口",加减后首页状态、检查页问题、我的页总量同步刷新。

7、多维度方案选型对比:分类管理页的加减交互

在实际的架构研讨中,有同学曾经发出过极其尖锐的质疑:“物资页为什么不直接用首页的列表?为什么要单独做分类管理?”

为了彻底解答这个选型问题,我们将系列中"分类 + 管理"型页面对比:

维度 App 60 物资页 App 59 药品页 App 54 订阅页 App 42 树洞
核心定位 库存加减 库存告警 续费管理 话题互动
分类方式 三分类胶囊 分类胶囊 话题网格
管理控件 − 数量 + 步进器 只读列表 开关卡 抱抱互动
主题色 橙棕 #D35400 #E74C3C

60 的差异化在于**“− 数量 + 步进器”**——物品列表右侧的"减号灰圆 / 数字 / 加号橙圆"是库存的快速增减控件(stepper),这是系列中唯一的"步进器"交互(其他管理页用开关/胶囊,本页用 ± 按钮直接调数量)。因为应急物资的库存调整是高频且简单的操作(买了罐头 +1、用了电池 −1),步进器"点一下调一"最直接,比进详情页改数字快得多。

点击加号增加库存

8、避坑指南:物资页的四个防翻车细节

在将这套看起来优雅的物资页设计推向真实生产环境时,我们踩过了几个深浅不一的暗坑。以下是内部复盘后沉淀的防翻车铁律:

第一,步进器逻辑必须实现。− + 按钮当前无点击逻辑——产品化必须实现增减(− 减一、+ 加一),且"数量 ≥ 0"边界(− 到 0 时禁用或提示"库存为 0"),增减后首页/检查页的状态联动刷新(6 → 5 仍偏少、6 → 12 充足)。如果按钮点了没反应,用户会以为应用坏了。

第二,步进器的点击热区。− + 按钮 28×28(约 28 像素)——真机上手点面积偏小(无障碍规范最小 44×44),产品化应扩大热区(按钮容器 44×44、图标视觉 28)或用整行点击区域。这是移动端最容易忽略的无障碍细节。

第三,库存状态判定联动。物资页只有数量无状态——产品化后按库存量判定状态(与首页/检查页同一状态机:低于建议量偏少、接近 0 不足),物资页加减后三页状态同步,避免"物资页 6 件、首页还显示充足"的矛盾。

第四,分类切换的数据写入隔离cats[activeCat].items 直接索引——产品化后注意"新增物品/编辑库存"要写到正确的分类数组,不能写到当前未选中的分类里,否则用户切回去发现数据"丢了"(其实写到别的分类了)。

滚动查看物品列表

9、常见问题 FAQ

问:为什么只有 3 个分类?

答:应急物资的三大类:食品饮水(生存必需:水/罐头/饼干)、照明电源(断电应急:手电筒/电池/蜡烛)、医疗急救(伤病处理:急救包/常用药)——3 类覆盖应急场景的核心需求(吃、亮、治),更多类(如衣物/工具)可产品化扩展。3 分类等分胶囊一行排开,切换直观。

问:− 和 + 按钮是干什么的?

答:库存数量调整——买了新物资点 +(库存 +1)、消耗了物资点 −(库存 −1),是库存的"快速增减"(步进器)。例如买了 6 罐罐头库存变 12,点两下 + 即可,比进详情页改数字高效。产品化后增减联动首页/检查页的状态(6 → 12 从"偏少"变"充足")。

问:点击分类胶囊后库存会丢失吗?

答:不会——三个分类的物品数据独立存储(cats 数组包含三个分类的 items),切换分类只是"显示不同的列表"(cats[activeCat].items),食品饮水的库存(24/6/10)切到照明电源(2/12/8)再切回来数据不变,分类是"视图过滤"而非"数据清空"。

问:为什么物资页用步进器而不是输入框?

答:因为应急物资的库存调整是"少量频繁"的操作(买了一罐罐头 +1、用了一节电池 −1)——步进器"点一下调一"比输入框(要打字)快得多,且步进器有"数量边界"(不能减到负数)天然防止误操作,适合库存这种"整数 + 小幅变化"的数据。

10、总结

纵观本篇对 60-emergency-kit 物资页(Func1Tab,102 行)的逐块拆解,@State 驱动的分类切换与步进器交互绝对不是大家在走马观花浏览 ArkUI 文档时所以为的"仅仅只是改个变量"那么简单。

物资页用三个 @Builder 块完成了"分类导航 + 库存管理"的组合布局:Header 点明职能、CatRow 给出三分类胶囊(选中橙底橙边)、ItemList 给出步进器列表(− 数量 + 灰减橙加)。整页的交互围绕"分类 + 步进"展开——分类选中态双属性变化(底边同橙)、步进器灰减橙加(主次分明)、数量居中稳定(增减不动),用户一页完成"看分类、调库存"。

从演示到产品,物资页的进化方向清晰:建议库存判定、消耗记录、购买计划联动、人口适配,从"记库存"走向"智能补给"。步进器三件套是本页的核心交互,把应急物资的库存维护变成"点一下"的轻操作。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐