OpenHarmony 英语学习 App 实战:基于艾宾浩斯曲线的单词复习系统设计
OpenHarmony 英语学习 App 实战:基于艾宾浩斯曲线的单词复习系统设计
摘要
背单词最难的不是“第一次记住”,而是“过几天还记得”。所以英语学习 App 不能只做词汇列表,还需要一套复习调度系统。本文以「英语视界 YingYu」项目为例,分享如何在 OpenHarmony/HarmonyOS 应用中实现基于 SM-2 思路的间隔重复复习系统。🧠📚
项目相关文件包括:
entry/src/main/ets/utils/SpacedRepetition.ts
entry/src/main/ets/pages/ReviewCenter.ets
entry/src/main/ets/utils/StorageService.ts
entry/src/main/ets/model/DataModels.ts
本文会从数据模型、算法计算、复习记录、待复习查询、翻转卡片交互几个角度展开。
一、为什么要做间隔重复?
如果用户每天学习 10 个新词,30 天就是 300 个。没有复习机制的话,用户很快会发现:学过的单词不断遗忘,学习成就感下降。
间隔重复的核心思想是:
- 刚学会的单词很快复习;
- 熟悉的单词延长复习间隔;
- 忘记的单词重新拉回短间隔;
- 每次复习结果都会影响下一次复习时间。
这比固定“每天复习全部单词”更高效。
二、复习记录数据模型
项目中使用 ReviewRecord 描述一个单词的复习状态:
export interface ReviewRecord {
wordId: string
wordType: 'vocabulary' | 'custom' | 'funEnglish' | 'grammar'
learningDate: string
reviewDates: string[]
nextReviewDate: string
easeFactor: number
interval: number
repetitions: number
}
字段说明:
wordId:单词 ID;wordType:单词来源;learningDate:首次学习日期;reviewDates:历史复习日期;nextReviewDate:下次复习日期;easeFactor:难度因子;interval:当前间隔天数;repetitions:连续记住次数。
这几个字段足够支撑一个轻量级复习系统。
三、SM-2 参数设计
在 SpacedRepetition.ts 中定义了两个关键参数:
const MIN_EASE_FACTOR = 1.3
const INITIAL_EASE_FACTOR = 2.5
easeFactor 可以理解为“这个词对用户来说有多容易”。越容易,下一次复习间隔增长越快;越难,间隔增长越慢。
项目还定义了 0-5 的质量等级:
// 0: 完全忘记
// 1: 错误,但看到答案后想起
// 2: 错误,但感觉快想起来了
// 3: 正确,但需要一些努力
// 4: 正确,很快想起
// 5: 正确,非常轻松
虽然当前 UI 中使用“认识/不认识”的简化交互,但算法层保留质量等级,有利于后续扩展更精细的复习反馈。
四、计算下一次复习时间
核心函数是 calculateNextReview():
function calculateNextReview(
record: ReviewRecord,
quality: number
): { nextDate: string, interval: number, easeFactor: number, repetitions: number } {
let { easeFactor, interval, repetitions } = record
easeFactor = easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
if (easeFactor < MIN_EASE_FACTOR) {
easeFactor = MIN_EASE_FACTOR
}
if (quality < 3) {
repetitions = 0
interval = 1
} else {
if (repetitions === 0) {
interval = 1
} else if (repetitions === 1) {
interval = 6
} else {
interval = Math.round(interval * easeFactor)
}
repetitions++
}
interval = Math.min(interval, 30)
const nextDate = new Date()
nextDate.setDate(nextDate.getDate() + interval)
return {
nextDate: nextDate.toISOString().split('T')[0],
interval,
easeFactor,
repetitions
}
}
这段逻辑有几个关键点:
- 回答错误:重置连续次数,间隔回到 1 天;
- 第一次记住:1 天后复习;
- 第二次记住:6 天后复习;
- 后续记住:根据
interval * easeFactor拉长间隔; - 最大间隔限制为 30 天,避免复习间隔过长。
对学生学习 App 来说,限制最大间隔很实用,因为中小学生词汇量和课程节奏通常需要更频繁的巩固。
五、记录一次复习结果
当用户完成一次复习时,调用 recordReview():
export function recordReview(
wordId: string,
wordType: ReviewRecord['wordType'],
quality: number
): ReviewResult {
let record = getReviewRecord(wordId)
if (!record) {
record = createReviewRecord(wordId, wordType)
}
const result = calculateNextReview(record, quality)
const today = new Date().toISOString().split('T')[0]
const reviewDates = [...record.reviewDates, today]
const updatedRecord: ReviewRecord = {
...record,
reviewDates,
nextReviewDate: result.nextDate,
interval: result.interval,
easeFactor: result.easeFactor,
repetitions: result.repetitions
}
updateReviewRecord(wordId, updatedRecord)
return {
wordId,
quality,
newRecord: updatedRecord
}
}
这里遵循了清晰的数据流:
- 获取已有复习记录;
- 没有记录则创建;
- 根据质量等级计算下一次复习;
- 写入今天的复习日期;
- 更新持久化记录;
- 返回更新后的结果。
六、创建复习记录
复习记录由 StorageService.ts 创建:
export function createReviewRecord(wordId: string, wordType: ReviewRecord['wordType']): ReviewRecord {
const records = getReviewRecords()
const today = new Date().toISOString().split('T')[0]
const nextDate = new Date()
nextDate.setDate(nextDate.getDate() + 1)
const newRecord: ReviewRecord = {
wordId,
wordType,
learningDate: today,
reviewDates: [],
nextReviewDate: nextDate.toISOString().split('T')[0],
easeFactor: 2.5,
interval: 1,
repetitions: 0
}
records.push(newRecord)
saveReviewRecords(records)
return newRecord
}
首次学习后,默认安排第二天复习,这符合记忆曲线的基本规律。
七、查询今日待复习单词
待复习查询逻辑非常直接:
export function getDueReviewWords(): ReviewRecord[] {
const records = getReviewRecords()
const today = new Date().toISOString().split('T')[0]
return records.filter(r => r.nextReviewDate <= today && !r.wordId.startsWith('_mastered_'))
}
只要 nextReviewDate <= today,就说明该单词已经到复习时间。
这里还过滤了 _mastered_ 开头的记录,便于后续处理“已掌握”或特殊状态。
八、今日复习统计
首页或复习中心可以通过 getTodayReviewStats() 展示复习进度:
export function getTodayReviewStats(): {
total: number,
due: number,
reviewed: number,
remaining: number
} {
const records = getReviewRecords()
const today = new Date().toISOString().split('T')[0]
const due = records.filter(r => r.nextReviewDate <= today && !r.wordId.startsWith('_mastered_')).length
const reviewed = records.filter(r => r.reviewDates.includes(today)).length
const total = records.length
const remaining = Math.max(0, due - reviewed)
return { total, due, reviewed, remaining }
}
再进一步生成提醒文案:
export function getReviewReminderText(): string {
const stats = getTodayReviewStats()
if (stats.due === 0) {
return '今日复习已完成'
}
if (stats.remaining === 0) {
return `今日复习已完成 ${stats.reviewed} 个`
}
return `还有 ${stats.remaining} 个单词等待复习`
}
这种文案可以放在首页、通知、实况窗或每日任务卡片中。
九、复习中心:翻转卡片交互
ReviewCenter.ets 中实现了复习页面。页面先加载到期复习单词:
loadReviewWords() {
const dueRecords = getDueReviewWords()
const allWords = getCustomWords()
const wordsMap = new Map<string, CustomWord>()
for (let i = 0; i < allWords.length; i++) {
const w = allWords[i]
wordsMap.set(w.id, w)
}
const items: ReviewItem[] = []
for (let i = 0; i < dueRecords.length; i++) {
const record = dueRecords[i]
const word = wordsMap.get(record.wordId)
if (word) {
items.push({ word: word, record: record })
}
}
this.reviewItems = items
this.totalCount = this.reviewItems.length
}
这里把复习记录和单词详情合并成 ReviewItem,方便 UI 展示。
十、卡片翻转状态
复习中心使用 isFlipped 控制正反面:
flipCard() {
animateTo({
duration: 300,
curve: Curve.EaseOut
}, () => {
this.isFlipped = !this.isFlipped
})
}
正面展示英文和音标:
Text(item.word.word)
.fontSize(40)
.fontWeight(FontWeight.Bold)
if (item.word.phonetic) {
Text(item.word.phonetic)
.fontSize(20)
.margin({ top: 8 })
}
反面展示释义和例句:
Text(item.word.translation)
.fontSize(28)
.fontWeight(FontWeight.Bold)
if (item.word.example) {
Text(item.word.example)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ top: 20 })
}
翻转卡片非常适合背单词:先回忆,再查看答案,最后判断是否认识。
十一、认识/不认识:简化用户反馈
当前 UI 中通过两个按钮让用户反馈:
Button('不认识')
.onClick(() => this.handleReview(false))
Button('认识')
.onClick(() => this.handleReview(true))
handleReview() 根据结果更新间隔:
if (remembered) {
newRepetitions++
newInterval = Math.ceil(newInterval * newEaseFactor)
newEaseFactor = Math.max(1.3, newEaseFactor + 0.1)
} else {
newRepetitions = 0
newInterval = 1
newEaseFactor = Math.max(1.3, newEaseFactor - 0.2)
}
这是一种更适合低龄用户的交互:不要求用户判断 0-5 分,只要选择“认识/不认识”。
十二、小结
本文结合「英语视界 YingYu」项目,拆解了一个 OpenHarmony 英语学习 App 的复习系统:
- 使用
ReviewRecord保存复习状态; - 通过
easeFactor、interval、repetitions控制复习节奏; - 使用 SM-2 思路计算下一次复习日期;
- 通过
getDueReviewWords()查询今日待复习单词; - 使用翻转卡片完成“回忆 - 查看 - 判断”流程;
- 通过“认识/不认识”降低操作成本。
背单词功能不是词库越大越好,真正关键的是让用户在正确的时间复习正确的单词。间隔重复系统,就是英语学习 App 的核心发动机之一。🚀

更多推荐



所有评论(0)