欢迎光临
我们一直在努力

HarmonyOS Preferences 最近浏览越存越多怎么办:中式美食怎么做去重、截断和时间更新

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最近浏览看起来是小功能,但它很容易拖慢页面。中式美食详情页每打开一道菜都会记录浏览,如果不去重、不限制数量,Preferences 里会越塞越多,首页最近浏览区也会变得不可信。我的处理原则很直接:页面先把用户看到的问题挡住,数据层再把真正的边界兜住,最后用可重复的操作去验收。这样写出来的逻辑不一定最炫,但上线后更稳。

这个问题的原因通常不是某一行代码写错,而是页面状态、业务口径和数据回读没有分开。只要原因判断错了,后面修起来就会变成哪里闪修哪里,最后越修越乱。

项目说明
应用 中式美食
页面 最近浏览
技术栈 HarmonyOS、ArkUI、ArkTS、本地数据
主要问题 状态口径不清楚,页面显示和本地数据对不上
处理目标 用户重复操作、返回页面、刷新列表时结果保持一致
环境版本/口径
DevEco Studio 6.1.0 Release
HarmonyOS SDK API 18
存储方式 Preferences
页面入口 菜谱详情页、首页最近浏览区
验收重点 去重、截断、更新时间、重新进入后顺序一致

为什么这个问题会出现

很多页面刚开始能跑,是因为测试路径太顺了:点一次、等结果、再看页面。但真实使用里,用户不会按我们的节奏来。用户会连续点,会返回再进来,会改条件后又取消,也会在网络慢的时候以为自己没点上。

常见写法当时看起来没问题后面的问题
页面变量直接改 代码短,马上能看到效果 Repository 失败后页面不知道怎么回滚
不记录动作来源 所有入口走同一段逻辑 返回列表时不知道该刷新哪一块
不做兜底校验 单次测试能通过 重复点击或旧数据会把状态弄乱
只看 UI 不看数据 页面像是对了 下次进入又变成旧状态

我现在写这类功能,会先问自己一个问题:这个状态到底是谁负责?页面负责展示,Repository 负责数据口径,路由负责入口来源。只要这三件事混在一起,后面必然难排查。

页面层先把状态说清楚

在 最近浏览 里,我不会让一个布尔值承担所有含义。页面里至少要区分“正在处理”“当前展示结果”“已经确认的数据”。这样出问题时能知道到底是哪一步错了。

type PageStatus = 'idle' | 'loading' | 'saving' | 'error';

@State private pageStatus: PageStatus = 'idle';
@State private currentItems: RecipeItem[] = [];
@State private errorMessage: string = '';

private canTriggerAction(): boolean {
return this.pageStatus !== 'loading' && this.pageStatus !== 'saving';
}

这段代码不是为了显得复杂,而是为了避免页面状态互相抢。比如正在保存时,不应该再触发同一个写动作;加载失败时,也不能直接展示空列表,否则用户会以为真的没有数据。

最近浏览不要只会 push

最近浏览最常见的错误,就是每进一次详情页就往数组里 push 一条。这样短期看能用,时间一长就会出现同一道菜重复很多次,首页最近浏览区也会被旧数据撑大。

interface ViewedRecipe {
recipeId: string;
recipeName: string;
coverResource: string;
viewedAt: number;
}

const RECENT_VIEW_LIMIT = 30;
const RECENT_VIEW_KEY = 'recent_viewed_recipes';

我会先把数据结构定清楚:最近浏览不是完整菜谱表,只保存首页入口需要展示和跳转的字段。这样 Preferences 里的内容轻一点,读取也快一点。

写入时先去重,再放到最前面

中式美食详情页每次打开菜谱时,都会更新最近浏览。但更新不是追加,而是“旧记录移除,新记录放最前面,超过上限就截断”。

async recordViewedRecipe(recipe: RecipeItem): Promise<void> {
const list = await this.readViewedRecipes();
const nextItem: ViewedRecipe = {
recipeId: recipe.id,
recipeName: recipe.name,
coverResource: recipe.coverResource,
viewedAt: Date.now()
};

const filtered = list.filter(item => item.recipeId !== recipe.id);
const nextList = [nextItem, filtered].slice(0, RECENT_VIEW_LIMIT);

await this.preferences.put(RECENT_VIEW_KEY, JSON.stringify(nextList));
await this.preferences.flush();
}

这段代码解决的是三个问题:同一道菜不会重复出现,最新浏览一定排在最前面,Preferences 不会无限膨胀。这个口径比单纯 push 稳很多。

步骤做什么解决的问题
读取旧列表 拿到已有浏览记录 保留其他菜谱顺序
filter 去重 移除同一个 recipeId 防止重复卡片
放到头部 更新最新浏览时间 新看的内容靠前
slice 截断 限制最多 30 条 防止 Preferences 越存越大

读取时要容错旧数据

Preferences 里存的是字符串,读取时不能假设它永远是正确 JSON。应用升级、字段变更、手动清缓存,都可能让读取失败。

async readViewedRecipes(): Promise<ViewedRecipe[]> {
const raw = await this.preferences.get(RECENT_VIEW_KEY, '[]') as string;

try {
const list = JSON.parse(raw) as ViewedRecipe[];
return list
.filter(item => item.recipeId && item.recipeName)
.sort((left, right) => Number(right.viewedAt || 0) Number(left.viewedAt || 0))
.slice(0, RECENT_VIEW_LIMIT);
} catch (error) {
await this.preferences.put(RECENT_VIEW_KEY, '[]');
await this.preferences.flush();
return [];
}
}

这里的 catch 不是为了吞错误,而是为了让最近浏览不要拖垮首页。最近浏览是辅助入口,它坏了应该清空重来,不能影响菜谱列表和详情页主流程。

业务 key 要按真实含义来定

最近浏览越存越多怎么办? 不能靠一个全局变量糊过去。中式美食里我会给关键动作一个业务 key,这个 key 必须能看懂它锁住的是哪件事。

private buildActionKey(recipeId: string): string {
return `viewedAt:${recipeId}`;
}

private async runWithActionLock(recipeId: string, task: () => Promise<void>): Promise<void> {
const key = this.buildActionKey(recipeId);
if (this.runningKeys.has(key)) {
return;
}

this.runningKeys.add(key);
try {
await task();
} finally {
this.runningKeys.delete(key);
}
}

这里的重点是 finally。只要是异步动作,就一定要考虑失败和异常。否则按钮锁住以后不释放,用户只能退出页面重进。

Repository 层再兜一次

页面层能挡住大部分误操作,但它不是最后防线。以后功能多了,同一段 Repository 可能被详情页、列表页、服务卡片、导入逻辑一起调用。如果 Repository 自己不判断,别的入口一样能写出脏数据。

export class ViewedRecipeRepository {
async save(recipeId: string, payload: Record<string, string>): Promise<void> {
const oldRecord = await this.findByRecipeId(recipeId);

if (oldRecord) {
await this.update(oldRecord.id, {
payload,
updatedAt: Date.now()
});
return;
}

await this.insert({
recipeId,
payload,
createdAt: Date.now(),
updatedAt: Date.now()
});
}
}

这段逻辑表达的是一个简单口径:同一道菜相关的状态,先看有没有旧记录。有就更新,没有再插入。不要每次都新增,否则数据会越来越脏。

层级负责内容为什么不能省
ArkUI 页面 按钮、弹窗、列表展示 让用户知道当前动作有没有进行中
ViewModel 组织页面状态和动作 key 避免一个变量承担太多含义
Repository 查重、更新、插入 防止其他入口绕过页面写脏数据
验收用例 重复点、返回、刷新、异常 确认不是单次路径刚好成功

返回页面后要读真实数据

很多状态问题不是发生在当前页面,而是发生在返回后。比如详情页里保存成功了,列表页如果还拿旧数组显示,用户看到的就是错的。

async onPageShow(): Promise<void> {
await this.reloadVisibleState();
}

private async reloadVisibleState(): Promise<void> {
const latest = await this.repository.queryCurrentState();
this.currentItems = this.mergeState(this.currentItems, latest);
}

我不建议让详情页直接去改列表页内部数组。短期看省事,长期看会让入口越来越乱。更稳的做法是:写操作完成后,相关页面自己回读一次需要展示的状态。

为什么这样能解决问题

这个方案能解决问题,不是因为代码多写了几行,而是因为每一层都有自己的责任。页面层解决的是“用户现在能不能继续点”的问题,Repository 解决的是“这条业务数据到底应该新增还是更新”的问题,页面返回后重新读取解决的是“别的页面看到的是不是最新结果”的问题。

如果只做页面层,重复点击少了,但别的入口仍然可能写脏数据。如果只做 Repository,数据是干净了,但用户点按钮时没有反馈,会以为应用卡住。如果只做返回刷新,页面看起来会恢复,但中间写错的数据已经进库了。三层合起来,才是一个比较稳的处理方式。

只做一层还能留下什么坑现在怎么避免
只禁用按钮 页面重建或其他入口可能绕过 Repository 再查重和更新
只查重入库 用户连续点时没有反馈 ArkUI 层给 busy 状态
只返回刷新 数据写错后只是重新显示错数据 写入前先定业务口径
只靠人工测试 慢请求和旧数据覆盖测不出来 固定重复点击和返回刷新用例

以后怎么避免同类问题

后面再写中式美食的页面,我会先把动作分成两类:只影响展示的动作,和会改本地数据的动作。只影响展示的动作可以轻一点,比如切换 tab、展开说明;会改本地数据的动作就必须按上面的方式处理,不能只写一个 onClick。

我的检查清单也会固定下来:

检查项不通过时会出现什么
是否有业务 actionKey 连续操作时不知道该拦哪一个动作
是否有 finally 释放状态 异常后按钮可能一直不可用
Repository 是否查重 别的入口会写出重复数据
返回页面是否回读 列表状态和详情页状态可能不一致
失败提示是否具体 用户不知道是保存失败还是页面失败

这样做的好处是,问题以后再出现时,我不用从整页代码里乱找。先看 actionKey,再看 Repository 口径,再看返回刷新,基本就能定位到是哪一层没有守住。

还有一个容易忽略的小点:不要把这类状态只写在注释里。真正有用的是把口径落到变量名、方法名和测试动作里。比如 reloadVisibleState() 比 refresh() 更清楚,buildActionKey() 比 getKey() 更清楚,save() 里面先 findByRecipeId() 再 insert() 也比直接写一串数据库调用更容易看懂。

代码写清楚以后,后面自己回来排查也快。看到列表乱了,就先查 query 的排序;看到按钮乱了,就先查 actionKey;看到数据重复了,就先查 Repository 的查重逻辑。这个顺序固定下来,排查效率会高很多。

private async verifyAfterAction(recipeId: string): Promise<void> {
const pageState = this.currentItems.find(item => item.id === recipeId);
const dbState = await this.repository.findByRecipeId(recipeId);

if (!pageState || !dbState) {
this.errorMessage = '页面状态和本地数据没有对上,需要重新加载';
await this.reloadVisibleState();
}
}

这段验收代码不一定要原样放到生产逻辑里,但它提醒我:每个会改数据的动作,都要能回答“页面看到的”和“本地查到的”是不是同一件事。

我会怎么验证

只看一次正常点击没有意义,必须按容易出错的路径测。

验收动作预期结果
快速连续触发同一个动作 只有一次有效写入
写入过程中返回再进入 页面从 Repository 读到真实状态
取消操作或失败重试 临时状态不会污染正式结果
旧数据缺少字段 兜底排序或默认值不让页面崩
刷新三次列表 展示顺序和状态保持一致

最后说一句

最近浏览越存越多怎么办? 表面是页面问题,实际是状态边界问题。中式美食这类内容应用,页面入口多、状态也多,如果只靠一次点击后的临时变量,后面一定会遇到列表刷新不对、按钮状态不对、数据重复的问题。

我的做法就是把事情拆开:页面负责当前交互,ViewModel 负责动作节流和状态整理,Repository 负责真实数据口径。这样以后功能继续加,问题也能沿着这条线查,不会变成到处猜。

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS Preferences 最近浏览越存越多怎么办:中式美食怎么做去重、截断和时间更新
分享到: 更多 (0)

评论 抢沙发

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