【时光清单|11】HarmonyOS ArkTS 每日语录实战:从本地仓库稳定生成首页内容

首页上的“每日一言”只有一张卡片,却同时涉及数据来源、当天稳定性、随机刷新、收藏状态、空集合兜底和 ArkUI 响应式更新。最常见的实现是页面启动时直接 Math.random() 取一句,但这样每次重建页面都可能变化;另一种做法是把当天日期作为下标,却忽略时区边界,导致用户在本地午夜后仍看到昨天内容。再往后加收藏,如果只改页面副本,仓库状态和界面状态又会分叉。

时光清单 的真实源码采用进程内仓库方案:MockData.ets 提供 15 条语录,QuoteRepository.ets 通过单例保存内存数组,getTodayQuote() 按天计算索引,getRandomQuote() 支持“换一句”,toggleCollect() 修改收藏布尔值;AppViewModel 把这些能力暴露给 HomeView,最后由 QuoteCard 渲染正文、作者和交互。

本文先复核这条当前源码链路,不把未执行的构建、设备运行或内容授权检查写成结果,再分析它已经解决的问题和仍存在的边界:当前日索引使用 UTC 毫秒日而非本地日,随机刷新可能抽到同一句,收藏只存在内存中,返回数组虽复制了容器却仍共享元素对象。后半部分给出本地日期键、确定性散列、无重复随机、收藏持久化和测试时钟等渐进方案;这些方案会明确标注为演进设计,不会伪装成现有功能。

每日语录本地数据链路主题封面

本文会回答:

  1. 仓库的惰性初始化如何让首页不依赖加载时序。
  2. “每日一句”和“随机一句”为什么是两个不同策略。
  3. 当前日期取模算法在哪个时区边界可能变化。
  4. ArkUI 页面为什么在收藏后重新构造 Quote
  5. 空仓库、重复随机、浅复制和内存收藏怎样处理。
  6. 如何在保持本地优先的前提下增加持久化与可测试性。

本文唯一标记:CSDN-SERIES:ALL-163208213

一、真实数据模型:五个字段承载展示与交互

Quote.ets 定义了语录结构:

export interface Quote {
  id: string;
  content: string;
  author?: string;
  date: number;
  collected: boolean;
}

字段职责很清楚:

字段 当前含义 页面用途
id 本地语录稳定标识 收藏定位与列表键
content 语录正文 卡片主体文本
author? 可选作者 有值时才显示
date 创建或装载时的毫秒时间 当前页面未直接展示
collected 内存收藏状态 决定按钮文字和颜色

作者使用可选字段,因此没有作者时组件不会渲染空署名。相比写入空字符串,可选字段更准确地表达“来源未提供”。collected 被放进实体,方便仓库和页面共享状态;但这也意味着收藏不只是 UI 状态,后续必须决定是否持久化。

模型还提供创建函数:

export function createQuote(
  content: string,
  author?: string
): Quote {
  let q: Quote = {
    id: `${Date.now()}_${
      Math.random().toString(36).slice(2, 6)
    }`,
    content: content,
    author: author,
    date: Date.now(),
    collected: false,
  };
  return q;
}

当前内置语录在 MockData 中直接定义,没有调用这个函数;它更适合未来新增自定义语录。毫秒时间加四位随机片段在本地低频创建中足够实用,但若引入批量导入或跨设备合并,应改用更稳定的 UUID 或服务端标识。

二、仓库单例:把初始化和选择策略收在一处

QuoteRepository 使用单例:

export class QuoteRepository {
  private static _instance: QuoteRepository | null = null;
  private quotes: Quote[] = [];
  private initialized: boolean = false;

  static getInstance(): QuoteRepository {
    if (!QuoteRepository._instance) {
      QuoteRepository._instance = new QuoteRepository();
    }
    return QuoteRepository._instance;
  }
}

首页、收藏入口或备份逻辑如果各自创建仓库实例,内存收藏状态会互不相通。单例至少保证当前进程内引用同一个 quotes 数组。initialized 又避免每次读取都重新加载内置数据并覆盖收藏状态。

async init(): Promise<void> {
  if (this.initialized) return;
  this.quotes = getMockQuotes();
  this.initialized = true;
}

这个初始化方法虽然标记为 async,内部当前没有异步 I/O。这样做仍有接口价值:未来改成读取 Preferences、关系型数据库或本地资源时,调用方方法签名不必变化。

需要注意,并发调用 init() 时当前逻辑没有 initPromise。由于 getMockQuotes() 同步执行,实际竞争窗口很小;若未来加入 await,两个调用可能同时通过 initialized === false。届时应缓存初始化 Promise。

三、首页调用链:页面只表达“我要今天的语录”

数据链路经过四层:

HomeView
  -> AppViewModel.loadTodayQuote()
    -> QuoteRepository.getTodayQuote()
      -> getMockQuotes()

AppViewModel 的实现保持意图化:

async loadTodayQuote(): Promise<Quote> {
  return this.quoteRepo.getTodayQuote();
}

async loadRandomQuote(): Promise<Quote> {
  return this.quoteRepo.getRandomQuote();
}

async toggleQuoteCollect(
  id: string
): Promise<boolean> {
  return this.quoteRepo.toggleCollect(id);
}

页面不需要知道数组在哪里、今日索引怎样计算,也不直接修改仓库集合。以后替换数据来源,页面仍可调用相同方法。

首页加载、换句与收藏三条状态流

HomeView 初始化了一个合法空对象:

@State todayQuote: Quote = {
  id: '0',
  content: '',
  date: Date.now(),
  collected: false
};

这保证首次构建时 QuoteCard 拿到完整类型,不需要处理 undefined。加载结束后,页面用仓库结果替换整个状态对象,ArkUI 能明确观察到引用变化。

四、每日选择算法:稳定性来自日期序号取模

真实算法如下:

async getTodayQuote(): Promise<Quote> {
  await this.init();
  if (this.quotes.length === 0) {
    return {
      id: '0',
      content: '',
      date: Date.now(),
      collected: false
    };
  }

  const dayIndex =
    Math.floor(Date.now() / 86400000) % this.quotes.length;
  return this.quotes[dayIndex];
}

它把 Unix 毫秒时间除以一天的毫秒数,向下取整得到自 1970 年以来的“UTC 日序号”,再对语录数量取模。同一个 UTC 日内,多次进入首页都会得到同一句;第二天索引向后移动一位,形成稳定轮播。

这一方案具有三个优点:

  • 不需要保存“今天选中了哪一句”。
  • 同一时刻计算结果确定,重启应用也一致。
  • 算法复杂度为常数,完全离线。

它也有一个明确边界:Date.now() / 86400000 按 UTC 午夜切换,不按用户本地午夜切换。在中国时区,索引通常在上午 8 点变化,而不是 0 点。因此“每天稳定”成立,“按本地自然日更新”还没有成立。

五、用本地日期键修正自然日边界

如果产品文案是“每日一言”,通常用户预期本地日期变化时更新。可以先生成本地日期键:

function getLocalDayKey(now: Date): string {
  const year = now.getFullYear();
  const month = String(
    now.getMonth() + 1
  ).padStart(2, '0');
  const day = String(
    now.getDate()
  ).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

随后把日期键转成稳定索引。不要直接用 new Date(year, month, day).getTime() / 86400000,夏令时地区的一天可能不是固定 24 小时。字符串散列更清晰:

function hashText(value: string): number {
  let hash = 2166136261;
  for (let i = 0; i < value.length; i++) {
    hash ^= value.charCodeAt(i);
    hash = Math.imul(hash, 16777619);
  }
  return hash >>> 0;
}

function selectDailyQuote(
  quotes: Quote[],
  now: Date
): Quote | null {
  if (quotes.length === 0) return null;
  const index =
    hashText(getLocalDayKey(now)) %
    quotes.length;
  return quotes[index];
}

Date 作为参数传入还有测试价值:不用修改系统时间,就能验证跨日行为。以上属于改造方案,当前源码仍使用 UTC 毫秒日取模。

六、语录集合变化时,取模序列为什么会整体漂移

日期序号对 quotes.length 取模,有一个常被忽略的特性:新增或删除一条语录后,模数变化,同一天映射到的内容也可能改变。应用升级当天,用户早上和下午可能看到不同语录,即使日期没变。

选择策略 同日稳定 数据集变化稳定 实现成本
日序号 % length
日期键散列 % length
日期键散列后按固定 ID 排序 部分
保存 dayKey -> quoteId
服务端下发每日 ID

本地优先应用可以在 Preferences 中保存当天选择:

export interface DailyQuoteSelection {
  dayKey: string;
  quoteId: string;
}

读取时先尝试按 quoteId 找语录;若条目已被删除,再重新选择。这样升级增加新内容不会改变当天结果。是否需要这层稳定性取决于产品承诺,不能为了一个小卡片引入过度复杂度。

七、随机刷新:当前算法允许连续抽中同一句

真实随机逻辑:

async getRandomQuote(): Promise<Quote> {
  await this.init();
  if (this.quotes.length === 0) {
    return {
      id: '0',
      content: '',
      date: Date.now(),
      collected: false
    };
  }

  const index = Math.floor(
    Math.random() * this.quotes.length
  );
  return this.quotes[index];
}

每个位置概率相同,但点击“换一句”后可能返回当前语录。算法没有错,体验却像按钮没生效。只要集合大于 1,可以排除当前 ID:

function getRandomExcept(
  quotes: Quote[],
  currentId: string
): Quote | null {
  if (quotes.length === 0) return null;
  if (quotes.length === 1) return quotes[0];

  const candidates = quotes.filter(
    (item: Quote) =>
      item.id !== currentId
  );
  const index = Math.floor(
    Math.random() * candidates.length
  );
  return candidates[index];
}

如果希望连续多次不重复,可以维护一个洗牌袋:将全部 ID 打乱,依次取出;袋子用完后再洗牌。相比不断重试随机数,洗牌袋能给出可验证的“不重复一轮”承诺。

八、空仓库兜底:合法对象并不等于合格内容

getTodayQuote()getRandomQuote() 都在数组为空时返回:

{
  id: '0',
  content: '',
  date: Date.now(),
  collected: false
}

这防止访问 this.quotes[index] 得到 undefined,也让函数始终满足 Promise<Quote>。但页面仍会渲染一张正文为空的卡片,并保留“换一句”和“收藏”操作。

更清晰的接口可以返回可辨识结果:

export type QuoteLoadResult =
  | {
      status: 'ready';
      quote: Quote;
    }
  | {
      status: 'empty';
    }
  | {
      status: 'error';
      message: string;
    };

页面分别展示内容、空状态和错误重试。若保持当前 Quote 返回类型,也应至少让 QuoteCard 根据 id === '0' 或空正文隐藏收藏按钮,并显示“暂无语录”。

HomeView、ViewModel、QuoteRepository、种子数据与建议持久层结构

空状态不是异常堆栈的替代品。真实仓库目前只读取同步内置数组,几乎不会失败;未来接入文件或持久化后,错误状态应与空集合分开。

九、返回副本:复制数组不等于隔离实体

getAll() 返回数组副本:

async getAll(): Promise<Quote[]> {
  await this.init();
  return [...this.quotes];
}

调用方执行 result.push(...) 不会改变仓库数组长度,这是好的。但数组中的 Quote 仍是同一对象引用:

const all = await repo.getAll();
all[0].collected = true;

上面的修改会影响仓库内第一条语录。若仓库要严格拥有状态,应返回实体副本:

function cloneQuote(item: Quote): Quote {
  return {
    id: item.id,
    content: item.content,
    author: item.author,
    date: item.date,
    collected: item.collected,
  };
}

async getAllSafe(): Promise<Quote[]> {
  await this.init();
  return this.quotes.map(
    (item: Quote) => cloneQuote(item)
  );
}

另一种方向是让 Quote 只读,通过仓库命令更新。关键不是深复制越多越好,而是明确谁可以修改实体。

十、收藏流程:共享对象会让页面再次反转

仓库按 ID 查找并切换:

async toggleCollect(id: string): Promise<boolean> {
  await this.init();
  const quote = this.quotes.find((q: Quote) => q.id === id);
  if (quote) {
    quote.collected = !quote.collected;
    return true;
  }
  return false;
}

首页点击后调用 ViewModel,再重新创建页面对象:

await this.viewModel.toggleQuoteCollect(
  this.todayQuote.id
);

let q: Quote = {
  id: this.todayQuote.id,
  content: this.todayQuote.content,
  author: this.todayQuote.author,
  date: this.todayQuote.date,
  collected: !this.todayQuote.collected
};
this.todayQuote = q;

这里不只是“没有判断返回值”。getTodayQuote()getRandomQuote() 都直接返回仓库数组里的对象,HomeView.todayQuote 因而可能与仓库持有同一个 Quote 引用。假设收藏前为 falsetoggleCollect() 先把共享对象改成 true;调用返回后,页面再读取 this.todayQuote.collected 时看到的已经是 true,随后取反得到 false。页面最终构造出的新对象可能回到旧显示,而仓库对象已经是新值。

如果传入不存在的 ID,问题又是另一条路径:仓库返回 false,页面仍会执行取反。当前内置集合非空且卡片 ID 来自仓库,常规路径通常能找到对象,但源码没有把“找到并更新”作为页面更新的前置条件。两条风险都来自返回契约太弱和实体引用共享,不能只靠一次重新赋值解决。

最小修正可以在调用前保存期望值,并检查布尔结果:

const nextCollected = !this.todayQuote.collected;
const changed = await this.viewModel
  .toggleQuoteCollect(this.todayQuote.id);

if (!changed) {
  return;
}

this.todayQuote = {
  id: this.todayQuote.id,
  content: this.todayQuote.content,
  author: this.todayQuote.author,
  date: this.todayQuote.date,
  collected: nextCollected
};

更稳的仓库接口可以直接返回更新后的实体副本,失败返回 null 或明确错误;页面只采用仓库确认后的值,不再自行推断。若随后接入 Preferences,还应先定义写入成功才更新,还是乐观更新后失败回滚。以上都是建议实现,当前源码仍是共享对象、布尔返回值和页面二次取反的组合。

十一、当前收藏只在内存里,重启后会复位

初始化数据中的每条语录都写着 collected: false。仓库没有调用 DataStoretoggleCollect() 也没有 flush()。因此收藏在当前应用进程内有效,应用重新初始化后会从 getMockQuotes() 再次加载,收藏全部复位。

这是必须准确描述的真实边界。若要持久化,不必保存所有内置正文,只保存收藏 ID 集合:

export interface QuotePreference {
  collectedIds: string[];
  lastDaily?: DailyQuoteSelection;
}

初始化时把收藏集合投影到内置语录:

function applyCollectedIds(
  quotes: Quote[],
  collectedIds: string[]
): Quote[] {
  const ids = new Set(collectedIds);
  return quotes.map((item: Quote) => ({
    id: item.id,
    content: item.content,
    author: item.author,
    date: item.date,
    collected: ids.has(item.id),
  }));
}

这种设计让内置正文随应用版本更新,用户状态单独持久化;不会因为保存了一份旧正文而阻止内容升级。

十二、并发收藏:快速点击需要序列化或禁用

如果收藏改成异步持久化,用户快速点击两次可能触发两个并发写操作。两个调用都读取旧值,再分别写入新值,最终结果不一定等于“切换两次回到原状态”。

页面可以在保存期间禁用按钮:

@State collectSaving: boolean = false;

private async collectCurrent(): Promise<void> {
  if (this.collectSaving) return;
  this.collectSaving = true;
  try {
    const updated =
      await this.viewModel
        .toggleQuoteCollect(
          this.todayQuote.id
        );
    if (updated) {
      this.todayQuote = {
        ...this.todayQuote,
        collected:
          !this.todayQuote.collected
      };
    }
  } finally {
    this.collectSaving = false;
  }
}

ArkTS 对对象展开和类型约束需结合项目编译规则;也可以继续显式列字段。仓库侧则可以维护写入队列,确保同一 ID 的操作顺序执行。对于当前纯内存同步变更,这个风险很低,但一旦接入持久化就应同步处理。

十三、首页加载状态:finally 只能保证结束,不能说明结果

真实首页加载:

private async loadData(): Promise<void> {
  try {
    this.anniversaries =
      await this.viewModel.loadAnniversaries();
    this.todayQuote =
      await this.viewModel.loadTodayQuote();
  } finally {
    this.isLoading = false;
  }
}

finally 确保结束加载状态,但如果纪念日仓库先抛错,语录就不会继续加载;异常还可能向生命周期调用链传播。两个数据源互不依赖,首页可独立处理:

private async loadQuote(): Promise<void> {
  try {
    this.todayQuote =
      await this.viewModel.loadTodayQuote();
  } catch (e) {
    this.todayQuote = {
      id: '0',
      content: '',
      date: Date.now(),
      collected: false
    };
  }
}

也可以并发执行并分别接收结果。这样纪念日失败不会让每日语录空白,语录失败也不会阻断事项列表。页面应维护 quoteLoadingquoteEmptyquoteError 等必要状态,而不是只用一个全局布尔值覆盖所有区域。

十四、组件边界:QuoteCard 只渲染,不选择内容

QuoteCard 接收 @Prop quote 与两个回调:

@Component
export struct QuoteCard {
  @Prop quote: Quote;
  onRefresh?: () => void;
  onCollect?: () => void;
}

组件负责:

  • 展示正文。
  • 有作者时展示署名。
  • 根据 collected 切换文字与颜色。
  • 将点击事件交给父页面。

组件不负责:

  • 计算今天是哪一句。
  • 访问仓库。
  • 保存收藏。
  • 决定空仓库如何恢复。

这种边界让卡片可在首页、详情页或分享预览中复用。后续增加加载或禁用状态,可以作为显式 Prop,而不是让组件偷偷获取全局单例。

当前回调还存在一个容易被忽略的异步边界:QuoteCardonRefreshonCollect 声明为 () => void,点击时直接调用;HomeView 实际传入的却是 async 回调。组件既不等待 Promise,也不知道操作何时结束,因此无法根据真实完成状态禁用按钮、显示进度或阻止连续点击。JavaScript 运行时可以执行这个函数,但“能够调用”不等于“交互已经具备异步协议”。

如果卡片需要负责点击期间的交互锁,回调可以显式声明为 () => Promise<void>,组件维护局部忙碌态并在 finally 恢复;如果忙碌态仍由页面统一拥有,则组件应接收 refreshingcollectSaving 等只读 Prop,并在禁用时拒绝再次触发。两种设计都比丢弃 Promise 更容易验证,选择哪一种取决于谁拥有操作生命周期。

十五、文本与版权:本地语录也要可维护

MockData 当前有 15 条内置内容,部分带作者、部分没有作者。工程上至少应做到:

  1. 每条 id 永久稳定,升级时不要随意重排或重用。
  2. 作者未知就省略,不猜测来源。
  3. 对外发布前复核引用准确性和使用授权。
  4. 正文需要最大长度,避免破坏卡片布局。
  5. 不在代码中混入用户私人内容。

可把内置数据移到资源文件并在仓库初始化时解析,但不要为了“可配置”引入网络依赖。离线资源可以随版本审核、测试和更新,也更符合本地优先定位。

十六、日期、语言与多设备一致性

当前算法只依赖系统时间和数组顺序。同一时刻、同一版本的两台设备通常会算出同一句,但以下情况会打破一致:

  • 两台设备时区不同。
  • 一台设备的系统日期不准确。
  • 应用版本不同,语录数组长度或顺序变化。
  • 收藏只在各自内存中,没有同步。

如果目标只是“每台设备当天稳定”,本地日期散列即可。如果产品承诺“所有设备同一天看到相同内容”,则必须定义统一时区和内容版本;如果还要同步收藏,则需要稳定用户身份、同步版本、删除规则和隐私说明。

不要把确定性算法误称为分布式同步。两台设备碰巧计算出相同结果,与两台设备共享同一状态,是完全不同的能力。

十七、测试时钟:不要让 Date.now 散落在领域逻辑

当前模型、MockData 和仓库都直接调用 Date.now()。测试“午夜前后”“指定日期映射”“空集合兜底”时会依赖真实时间。

可抽象最小时间接口:

export interface Clock {
  now(): number;
}

export class SystemClock implements Clock {
  now(): number {
    return Date.now();
  }
}

export class FixedClock implements Clock {
  constructor(
    private readonly value: number
  ) {}

  now(): number {
    return this.value;
  }
}

仓库构造时注入 Clock,生产环境使用 SystemClock,测试使用 FixedClock。这不是为了建立庞大框架,而是把“当前时间”从隐式全局输入变成可控制依赖。

历史证据与当前未验证项

本轮阅读的 PROJECT_ERRORS.md 有设置持久化、主题对比度、纪念日跨页刷新和功能完整性的日期记录,却没有找到每日语录的专门缺陷、迁移过程或验收结果。对 QuoteRepository.ets 做的聚焦 Git 历史检查也没有返回足以描述演进过程的提交。因此,本文只能陈述当前文件能证明的行为,不能虚构“曾经接入网络后改回本地”“收藏持久化已经修复”或“某次版本完成了时区切换”之类历史。

当前也没有执行 assembleHap,没有在模拟器或真机跨越本地午夜,没有重建应用进程验证收藏恢复,没有注入空集合与仓库失败,也没有快速连点“换一句”和“收藏”。项目错误记录中的历史构建通过对应其他 2026 年 5 月 20 日修复,不能挪作这条语录链的当前构建证据。

所以下一节的表格是建议的验收矩阵,表内“期望”表示未来实现或修正后应该观察到的结果,而不是本文已经跑出的测试报告。尤其是本地午夜更新、重启恢复、失败不误反转这三项,当前源码分别存在 UTC 日边界、无持久化和共享引用二次取反的问题,不能预先勾选为通过。

十八、验证矩阵:覆盖稳定、变化和失败

场景 操作 期望
同一天重复进入 多次读取 today 返回同一 ID
日期跨天 固定时钟推进一天 按策略切换
本地午夜 23:59 到 00:01 本地日策略及时变化
空集合 仓库无语录 返回空状态,不崩溃
单条集合刷新 点击换一句 仍返回唯一条目
多条集合刷新 排除当前 ID 不连续重复
收藏存在 ID 点击收藏 仓库与页面一致
收藏不存在 ID 传入错误 ID 页面不反转
重启恢复 重建仓库 持久化方案恢复收藏
返回所有 修改返回数组 不改变仓库容器
超长正文 资源含长文本 不遮挡按钮或溢出
深浅色 切换系统主题 正文、署名、按钮可读

纯算法测试应优先覆盖索引范围:

for (let day = 0; day < 1000; day++) {
  const index = day % 15;
  // index 应始终位于 0..14
}

页面测试再验证“换一句”“收藏”、加载空状态、长文本和重复点击。两类测试各自解决不同问题。

十九、常见故障与定位方法

现象 根因 处理
每天上午 8 点才换内容 使用 UTC 毫秒日 改用本地日期键
点击换一句没有变化 随机抽中当前项 排除当前 ID
重启后收藏消失 只修改内存数组 持久化收藏 ID
收藏不存在项仍变色 页面忽略返回值 成功后再更新 UI
修改 getAll() 结果影响仓库 只浅复制数组 返回实体副本
升级后当天内容变化 数组长度改变 保存当天 quoteId
首页某一区域失败全页空白 串行加载共用状态 分区加载与错误隔离
两设备收藏不同 没有同步协议 明确本地边界或设计同步

排查“每日不稳定”时先记录本地日期、UTC 日期、内容版本、语录数量和选中 ID,不要记录不必要的用户信息。

二十、发布前核对清单

  • [ ] Quote 五个字段与实际源码一致。
  • [ ] 内置语录数量与 MockData 一致,未虚构在线来源。
  • [ ] 同一计算日内重复进入返回相同语录。
  • [ ] 已说明当前算法按 UTC 毫秒日取模。
  • [ ] 空仓库不会访问越界。
  • [ ] “换一句”允许重复属于当前事实,改进方案单独标注。
  • [ ] 收藏只在内存中,未宣称跨重启保存。
  • [ ] 页面在仓库返回失败时不会错误反转状态。
  • [ ] 长正文与无作者内容能正常布局。
  • [ ] 首页语录失败不阻断其他主要内容。
  • [ ] 文中没有把当前进程内语录链路虚构成在线服务或账号同步。
  • [ ] 引用来源、作者署名与内容使用方式已人工复核。

二十一、总结:稳定内容依靠确定性,而不是缓存运气

时光清单 的真实实现把每日语录放在一条清晰链路中:MockData 提供本地集合,QuoteRepository 惰性初始化并统一执行每日选择、随机选择和收藏切换,AppViewModel 暴露页面意图,HomeView 持有响应式状态,QuoteCard 专注渲染。空数组时返回合法对象,getAll() 也避免调用方直接改变仓库数组长度。

这套实现适合当前离线、轻量的首页内容,但边界同样明确:每日索引按 UTC 日切换,集合变化会改变映射,随机刷新可能重复,收藏不会持久化,数组副本仍共享实体。渐进改造应从本地日期键、返回结果校验和收藏 ID 持久化开始,再按产品承诺决定是否需要当天选择记录、初始化 Promise、关系型存储或多设备同步。

真正稳定的“每日一句”不需要复杂网络服务,但需要把日期、数据版本、选择策略和状态所有权说清楚。只要这些边界明确,首页语录链路就能在离线条件下逐步变得可预测、可测试、可演进。


AI 辅助声明: 本文由 AI 辅助整理,所有当前实现结论均依据 QuoteRepository.etsQuote.etsMockData.etsAppViewModel.etsHomeView.etsQuoteCard.ets 的真实源码人工复核;本地日期键、持久化收藏和多设备协议为明确标注的演进建议。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐