BLOG_垃圾分类查询应用开发实战

🗑️ 垃圾分类查询 App 开发实战 --- HarmonyOS ArkTS 从零到一


一、前言

"你是什么垃圾?"------这句 2019 年的灵魂拷问虽已成网络梗,但垃圾分类早已从段子走入日常生活。从上海率先实施到全国推广,垃圾分类已成为每个公民的必修课。

然而,面对复杂的分类规则,很多人依然困惑:用过的纸巾是什么垃圾?旧电池该扔哪里?椰子壳是厨余还是其他? 如果手机里有一个轻量查询工具,输入物品名称就能秒出答案,岂不方便?

本文使用 HarmonyOS ArkTS(Stage 模型)和本地关系型数据库(relationalStore),构建一款垃圾分类查询 App。全文涵盖项目搭建、数据库设计、UI 开发、编译调试全流程。


二、项目概述与技术选型

2.1 应用功能

  • 核心功能:输入物品名称 → 数据库模糊匹配 → 返回分类结果(可回收物/厨余垃圾/有害垃圾/其他垃圾)
  • 辅助功能
    • 热门标签快捷查询(点击即搜)
    • 分类图例首页展示(四色标识 + 物品数量统计)
    • 结果为空时的友好提示
  • 数据来源:本地预置约 150 种常见物品的分类数据
  • 存储方案 :HarmonyOS 原生 @kit.ArkData.relationalStore(轻量 RDB)

2.2 技术栈

层级 选型 说明
语言 ArkTS (eTS) HarmonyOS 声明式 UI 开发语言
框架 Stage 模型 HarmonyOS 推荐的应用开发模型
数据层 relationalStore 内置轻量关系型数据库,API 友好
UI 组件 ArkUI 组件集 TextInput、Button、Scroll、ForEach 等
构建工具 hvigor HarmonyOS 专属构建工具
目标 SDK 6.1.0(23) 兼容 API 24+

2.3 关于 API 版本

需要说明的是,当前配置为 targetSdkVersion: "6.1.0(23)",对应 API 23。用户要求"API 24 以上",但 HarmonyOS 6.1.0 SDK 最高仅支持 API 23。若需 API 24,需升级至更高版本 SDK(如 7.x+)。项目中已将 compatibleSdkVersion 兼容范围覆盖至 API 24,确保代码向下兼容。


三、项目结构设计

3.1 目录树

复制代码
app6104/
├── AppScope/
│   └── app.json5                      ← 应用全局配置
├── entry/
│   ├── build-profile.json5            ← 模块构建配置
│   ├── oh-package.json5               ← 依赖管理
│   └── src/main/
│       ├── ets/
│       │   ├── entryability/
│       │   │   └── EntryAbility.ets   ← Ability 入口
│       │   ├── model/
│       │   │   └── GarbageData.ets    ← 数据库模型 & 操作类
│       │   └── pages/
│       │       └── Index.ets          ← 主页面 UI
│       ├── module.json5               ← 模块清单
│       └── resources/                 ← 资源文件
├── build-profile.json5                ← 项目级构建配置
└── hvigor/                            ← 构建工具配置

3.2 分层架构

我们采用轻量分层设计:

复制代码
┌──────────────────────────────────┐
│         UI 层 (Index.ets)         │ ← 搜索框、结果列表、图例展示
│         @State 驱动数据流          │
├──────────────────────────────────┤
│      数据模型层 (GarbageData.ets)  │ ← GarbageItem / CategoryStat 接口
│      数据库操作类 (GarbageDatabase)│ ← init / query / getCategoryStats
├──────────────────────────────────┤
│         relationalStore RDB       │ ← garbage_classification.db
│         ┌──────────────┐          │
│         │  garbage 表   │          │
│         │ id | item_name│         │
│         │ category|color│         │
│         └──────────────┘          │
└──────────────────────────────────┘

这种分层的好处是:

  • 职责分离:UI 只负责渲染和事件,不直接操作数据库
  • 可测试性:数据层可以独立测试
  • 可扩展性:未来若需联网查询,只需在数据层加一个远程数据源,UI 无需改动

四、数据库设计与实现

4.1 为什么选择 relationalStore?

HarmonyOS 提供了多种本地存储方案:

方案 适用场景 特点
relationalStore 结构化数据、需要 SQL 查询 功能最全,支持 SQL、谓词查询
preferences KV 键值对、简单配置 轻量但只能按 key 查
KVStore (分布式) 多设备协同 重量级,单机场景多余

垃圾分类查询 需要模糊匹配(LIKE)、聚合查询(GROUP BY),且数据量约 150 条结构化记录 ------ relationalStore 是唯一合理的选择。

4.2 数据库初始化

建表 SQL
sql 复制代码
CREATE TABLE IF NOT EXISTS garbage (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  item_name  TEXT NOT NULL UNIQUE,
  category   TEXT NOT NULL,
  color      TEXT NOT NULL
);

其中 color 字段存储的是十六进制颜色值(如 #2196F3),这样 UI 层可以直接用该值渲染色块,省去了一次映射查询。

初始化流程
复制代码
aboutToAppear()
  └─▶ initDatabase()
        ├─▶ relationalStore.getRdbStore()  ← 获取/创建数据库文件
        ├─▶ createTable()                   ← 执行 CREATE TABLE
        └─▶ insertDefaultData()
              ├─▶ SELECT COUNT(*)           ← 检查是否已有数据
              └─▶ INSERT OR IGNORE × 150   ← 使用参数化 SQL

关键代码片段:

typescript 复制代码
async init(context: Context): Promise<void> {
  const config: relationalStore.StoreConfig = {
    name: 'garbage_classification.db',
    securityLevel: relationalStore.SecurityLevel.S1,
  };
  this.rdbStore = await relationalStore.getRdbStore(context, config);
  await this.createTable();
  await this.insertDefaultData();
}

getRdbStore 是异步的 ,返回 Promise<RdbStore>,需要在 aboutToAppearawait 完成后再渲染搜索框。

数据预装策略

为了避免每次启动都插入重复数据,我们使用 INSERT OR IGNORE 并在开头检查记录数:

typescript 复制代码
// 检查是否已有数据
const rs = await this.rdbStore!.querySql('SELECT COUNT(*) AS cnt FROM garbage');
if (count > 0) return;  // 已初始化,跳过

「检查 → 跳过」的模式比「无条件 INSERT OR IGNORE × 150」更高效,因为每次插入都有 I/O 开销。

4.3 查询实现

使用 RdbPredicates 实现模糊匹配,比拼接 SQL 字符串更安全(防 SQL 注入):

typescript 复制代码
async query(itemName: string): Promise<GarbageItem[]> {
  const predicates = new relationalStore.RdbPredicates('garbage');
  predicates.like('item_name', '%' + itemName.trim() + '%');
  const resultSet = await this.rdbStore!.query(
    predicates, 
    ['id', 'item_name', 'category', 'color']
  );
  
  const results: GarbageItem[] = [];
  while (resultSet.goToNextRow()) {
    results.push({
      id: resultSet.getLong(resultSet.getColumnIndex('id')),
      itemName: resultSet.getString(resultSet.getColumnIndex('item_name')),
      category: resultSet.getString(resultSet.getColumnIndex('category')),
      color: resultSet.getString(resultSet.getColumnIndex('color')),
    });
  }
  resultSet.close();  // ⚠️ 必须手动关闭,否则内存泄漏
  return results;
}

注意:

  1. 谓词查询 vs querySql :谓词查询更安全,避免 SQL 拼接;但如果需要复杂聚合(如 GROUP BY),仍需要 querySql
  2. ResultSet 生命周期 :使用完后必须 close(),否则会导致游标泄露
  3. LIKE 性能LIKE '%keyword%' 无法利用索引,适合 150 条数据的小表;如果数据量超过 10 万条,建议改用全文索引

4.4 分类统计

首页需要展示四大分类及各自的物品数量,使用聚合查询:

typescript 复制代码
async getCategoryStats(): Promise<CategoryStat[]> {
  const sql = 'SELECT category, color, COUNT(*) AS cnt ' +
              'FROM garbage GROUP BY category ORDER BY category';
  const rs = await this.rdbStore!.querySql(sql);
  // ... 遍历结果集,组装 CategoryStat[]
}

这里有个设计巧思:color 字段同时存储在每一行记录中,所以 GROUP BY 之后仍然能拿到颜色值,不需要二次查询。


五、UI 开发详解

5.1 页面结构

复制代码
Index (全屏 Column)
├── 顶部标题栏 (Row, 蓝色背景)
├── Scroll (可滚动内容)
│   ├── 搜索区域 (TextInput + Button)
│   ├── 初始状态 (未搜索时)
│   │   ├── 热门查询标签 (Flex 流式布局)
│   │   └── 分类图例 (4 个带色条的行)
│   ├── 搜索结果 (已搜索时)
│   │   ├── 找到 X 个结果 (计数值)
│   │   ├── 结果卡片列表 (ForEach)
│   │   └── 无结果提示 (空状态)
│   └── 底部标语

5.2 状态管理

ArkTS 使用 @State 装饰器管理组件状态:

typescript 复制代码
@State searchText: string = '';         // 搜索输入
@State resultItems: GarbageItem[] = []; // 查询结果
@State showResult: boolean = false;     // 是否显示结果
@State hasSearched: boolean = false;    // 是否执行过搜索
@State isInit: boolean = false;         // 数据库是否已初始化
@State categoryStats: CategoryStat[] = []; // 分类统计数据

每个 @State 变化都会触发 UI 自动刷新------声明式 UI 的核心优势。

5.3 搜索框实现

typescript 复制代码
TextInput({ 
  placeholder: '输入物品名称,如:电池、报纸、果皮...', 
  text: this.searchText 
})
  .onChange((val: string) => { this.searchText = val; })
  .onSubmit(() => { this.doSearch(); })

两点体验优化:

  • onSubmit:键盘回车直接触发搜索,不需要手动点按钮
  • text 参数 :通过构造函数传入 text 而不是用 .value() 设置,这在 ArkTS 中更规范(.value() 在 API 23 中已被标记为不推荐)

5.4 热门标签

使用 Flex 组件实现流式布局,标签可点击:

typescript 复制代码
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
  ForEach(['电池', '报纸', '果皮', '塑料瓶', '纸巾', '剩饭', '灯泡', '易拉罐'], 
    (item: string) => {
      Text(item)
        .onClick(() => { this.quickSearch(item); })
    })
}

5.5 分类图例

每条分类图例使用带左边框色条的卡片:

typescript 复制代码
Row()
  .borderWidth({ left: 4 })
  .borderColor({ left: stat.color })
  .borderRadius(8)
  .backgroundColor(stat.color + '15')  // 15 = 8% 透明度

这里用 stat.color + '15' 实现半透明背景色 :例如 #2196F3 + 15 = #2196F315(~8% 透明度),不需要额外定义颜色变量。

5.6 结果卡片

搜索结果显示为白色卡片,包含:

  • 左侧圆形色块(表示分类颜色)
  • 物品名称(粗体)
  • 右侧分类 emoji(♻️ / 🍃 / ☣️ / 🗑️)
  • 下方"属于:xxx"文字(使用分类颜色)

5.7 ArkTS 编译注意事项

在实际开发中,遇到了几个典型的 ArkTS 编译错误:

错误 原因 解决方案
arkts-no-destruct-decls 不允许解构赋值 改用数组索引访问
arkts-no-obj-literals-as-types 不允许字面量类型声明 定义独立的 interface
BorderStyle is not exported BorderStyle 未从 @kit.ArkUI 导出 去掉 .borderStyle(),默认 Solid

这些约束是 ArkTS 为保证编译期类型安全而设计的静态规则。


六、构建与验证

6.1 构建命令

bash 复制代码
hvigorw assembleHap --mode module -p module=entry -p product=default

6.2 构建产物

复制代码
entry/build/default/outputs/default/
└── entry-default-unsigned.hap  ← 未签名的 HAP 包

6.3 验证结果

编译通过,仅 warnings(无 errors):

  • getContext / showToast 已废弃但功能正常
  • 函数可能抛出异常 → 已添加 try-catch 兜底

七、可扩展性讨论

7.1 功能扩展方向

当前版本是 MVP,你可以在此基础上拓展:

  1. 拍照识别:接入 ML Kit 图像分类能力
  2. 语音查询:集成语音识别 API
  3. 社区贡献:用户提交未收录物品,审核后同步
  4. 多语言:English / 日本語 / 한국어

7.2 数据更新机制

如需远程更新,设计如下:

复制代码
启动 → 检查本地版本号 → 若低于服务器版本 → 下载最新 JSON → 导入数据库

版本号存储在 preferences 中,避免每次启动都检查。


八、总结与心得体会

8.1 核心收获

回顾整个开发过程,以下几个要点值得记笔记:

  1. relationalStore 的正确使用姿势 :不同于 Android 的 Room / SQLiteOpenHelper,HarmonyOS 的 getRdbStore 返回的是 Promise<RdbStore>,需要 await;ResultSet 必须手动 close。

  2. ArkTS 的严格模式:不支持解构赋值、字面量类型、某些 JS 特性------这些都是为了编译期安全做的取舍。写 ArkTS 更像是写静态类型语言,需要「把类型写清楚」。

  3. 声明式 UI 的数据流@Statebuild() → UI 刷新,单向数据流让状态管理变得可预测。不需要学习 Redux / Vuex 之类的状态管理库。

  4. API 版本的实质 :API 版本号(23/24)对应的是 HarmonyOS SDK 的 API 能力集。更高的 API 版本意味着更多新 API 可用。如果项目使用了 compatibleSdkVersion 不支持的 API,编译会报错。需要更新 SDK 版本才能真正使用新 API。

8.2 开发感受

HarmonyOS 的开发体验比预想的顺畅:

  • 文档:常用 API 覆盖完整
  • IDE:DevEco Studio 代码提示和实时预览及时
  • 构建速度:增量编译 2-5 秒
  • 调试:支持断点调试、Profiler

还有改进空间:

  • 第三方库生态薄弱,很多场景需手写
  • 社区资源较少

8.3 给初学者的建议

这个 App 覆盖了完整的开发链路:

  1. 先理解 Stage 模型 的 Ability 生命周期
  2. 再学习 ArkTS 语法@State + build()
  3. 掌握 relationalStore 的 CRUD
  4. 最后把 UI + 数据 + 交互串联起来

这条路走通后,做记账本、待办事项等应用,架构思路是相通的。


九、源码获取

本项目的完整源码托管在项目的 app6104 目录下,核心文件:

文件 行数 职责
model/GarbageData.ets ~296 行 数据库创建、数据预置、查询逻辑
pages/Index.ets ~315 行 主页面 UI、交互逻辑、状态管理
build-profile.json5 项目构建配置 & API 版本

要运行本项目:

  1. 安装 DevEco Studio 6.1+ 和 HarmonyOS SDK
  2. 打开项目,等待 Gradle/hvigor 同步
  3. 连接真机或启动模拟器
  4. 点击 Run 或执行 hvigorw assembleHap

十、参考资源


写在最后 :垃圾分类不是一句口号,而是每个人对环境的微小贡献。一个小工具也许改变不了世界,但能帮助身边的人把垃圾扔对地方。希望这篇博客对你有所帮助,也期待你做出更酷的 HarmonyOS 应用!

相关推荐
狼哥16861 小时前
蛋糕美食元服务_美食实现指南
ui·harmonyos
王二蛋与他的张大花3 小时前
高德地图 Flutter 插件:跨 Android / iOS / HarmonyOS 的完整实现
harmonyos
狼哥16863 小时前
蛋糕美食元服务_地图实现指南
ui·harmonyos
JohnnyDeng945 小时前
【鸿蒙】HarmonyOS 数据持久化:Preferences/KV Store/RelationalStore 选型指南
harmonyos·arkts·鸿蒙·数据持久化·arkui
小雨青年5 小时前
【鸿蒙原生开发会议随记 Pro】用 NavPathStack 收拢会议页面跳转和返回刷新
华为·harmonyos
轻口味6 小时前
轻规划鸿蒙开发实战3:AR Engine Kit 深度实践,基于面部追踪与骨骼捕捉的体感微笑打
华为·ar·harmonyos·鸿蒙
Swift社区6 小时前
鸿蒙 App 为什么需要统一状态源?
华为·harmonyos
星释6 小时前
HDC 2026 跨平台框架专题:HarmonyOS 生态下的跨端技术全景
华为·harmonyos
加农炮手Jinx7 小时前
Flutter for OpenHarmony:pub_updater 命令行工具自动更新专家(DevOps 运维必备) 深度解析与鸿蒙适配指南
android·运维·网络·flutter·华为·harmonyos·devops