OpenHarmony 英语学习 App 实战:近场快传、跨端续学与全场景学习闭环

摘要

英语学习天然是一个跨场景任务:早上通勤路上用手机背几个单词,晚上回家用平板继续做语法练习,周末和同学互相分享学习成果。单设备 App 很容易形成“学习孤岛”,而 OpenHarmony/HarmonyOS 的全场景能力,正好可以把学习数据、学习状态和学习成果连接起来。📱➡️📲

本文以我的英语学习项目「英语视界 YingYu」为例,分享如何围绕学习场景设计 近场快传、跨端续学、分布式数据同步以及实况窗扩展思路。项目中已经包含 ShareKit 分享监听、DistributedKVStore 初始化、UIAbility onContinue() 续接能力等代码基础,非常适合写成一套全场景学习闭环。

一、全场景学习体验应该解决什么问题?

在教育类应用里,全场景并不是为了“炫技术”,而是为了解决真实学习问题:

  • 用户换设备后,学习进度不要丢;
  • 今日学习任务可以在系统级入口看到;
  • 学习报告可以快速分享给同学或家长;
  • 手机和平板之间能自然接续;
  • 网络不稳定时,基础学习数据仍然可用。

因此「英语视界」的全场景设计围绕三个关键词展开:

  1. 分享:用碰一碰/隔空传送快速分享学习报告;
  2. 同步:用分布式数据能力同步轻量学习数据;
  3. 续接:用 UIAbility 生命周期完成跨设备学习状态恢复。

二、近场快传:碰一碰分享学习报告

项目中封装了 ShareService.ts,通过 @kit.ShareKitharmonyShare 监听近场分享事件。

import { systemShare, harmonyShare } from '@kit.ShareKit'
import { uniformTypeDescriptor as utd } from '@kit.ArkData'

export function registerKnockShare(callback: (target: harmonyShare.SharableTarget) => void): void {
  try {
    harmonyShare.on('knockShare', (target: harmonyShare.SharableTarget) => {
      console.info('Triggered Knock Share')
      callback(target)
    })
  } catch (err) {
    console.error(`Register knock share failed: ${JSON.stringify(err)}`)
  }
}

除了碰一碰,还注册了隔空传送能力:

export function registerGesturesShare(callback: (target: harmonyShare.SharableTarget) => void): void {
  try {
    harmonyShare.on('gesturesShare', (target: harmonyShare.SharableTarget) => {
      console.info('Triggered Gestures Share')
      callback(target)
    })
  } catch (err) {
    console.error(`Register gestures share failed: ${JSON.stringify(err)}`)
  }
}

对于学习 App 来说,近场分享非常适合这些场景:

  • 同学之间分享今日学习成果;
  • 给家长展示连续打卡天数;
  • 分享自定义生词本;
  • 分享成就徽章;
  • 分享待复习任务。

三、生命周期中注册和注销分享监听

MainTabsV2.ets 中,页面出现时注册监听,页面销毁时注销监听。

aboutToAppear(): void {
  this.isTabletDevice = isTablet()
  this.isPhone = !this.isTabletDevice && deviceInfo.deviceType === 'phone'
  this.loadData()
  this.registerShareListeners()
}

aboutToDisappear(): void {
  this.unregisterShareListeners()
}

注册逻辑如下:

registerShareListeners(): void {
  if (this.isPhone) {
    registerKnockShare((target: harmonyShare.SharableTarget) => {
      handleShareFromKnock(target, 'learning_record')
    })

    registerGesturesShare((target: harmonyShare.SharableTarget) => {
      handleShareFromKnock(target, 'learning_record')
    })
  }
}

注销逻辑:

unregisterShareListeners(): void {
  if (this.isPhone) {
    unregisterKnockShare()
    unregisterGesturesShare()
  }
}

这里有两个设计点:

  • 只在手机设备上启用近场分享;
  • 页面离开时及时注销监听,避免资源长期占用。

这种写法更符合移动端能力使用习惯,也便于后续扩展到不同设备。

四、学习报告内容:不要直接分享冷冰冰的 JSON

分享内容不是技术数据,而是给用户看的学习成果。项目中定义了 LearningRecordShareData

export interface LearningRecordShareData {
  totalWords: number
  totalDays: number
  consecutiveDays: number
  achievementsCount: number
  achievementsTotal: number
  customWordsCount: number
  reviewDueCount: number
  todayProgress: string
  shareDate: string
}

然后把这些数据格式化成自然语言:

export function generateLearningRecordText(data: LearningRecordShareData): string {
  return `🎓 我的英语学习报告\n\n📅 ${data.shareDate}\n\n` +
    `✨ 已学单词: ${data.totalWords} 个\n` +
    `📅 学习天数: ${data.totalDays} 天\n` +
    `🔥 连续打卡: ${data.consecutiveDays} 天\n` +
    `🏆 获得成就: ${data.achievementsCount}/${data.achievementsTotal}\n` +
    `📚 自定义词: ${data.customWordsCount} 个\n` +
    `🔄 待复习: ${data.reviewDueCount} 个\n\n` +
    `💪 坚持学习,遇见更好的自己!\n` +
    `📱 使用「趣味英语」App 一起学习吧~`
}

这样的分享文案比 JSON 更适合传播,因为它具备:

  • 可读性;
  • 成就感;
  • 鼓励感;
  • 社交表达价值。

五、真正发起分享:SharedData + target.share

当系统触发近场分享后,项目会生成文本内容,并包装成 SharedData

export async function handleShareFromKnock(
  target: harmonyShare.SharableTarget,
  type: ShareContentType = 'learning_record'
): Promise<boolean> {
  try {
    const shareText = generateShareText(type)

    const shareData: systemShare.SharedData = new systemShare.SharedData({
      utd: utd.UniformDataType.TEXT,
      content: shareText
    })

    target.share(shareData)
    console.info('Knock/Gestures share initiated successfully')
    return true
  } catch (err) {
    console.error(`Knock share failed: ${JSON.stringify(err)}`)
    return false
  }
}

这里使用 TEXT 类型,是因为学习报告天然适合复制、转发和保存。后续也可以继续扩展:

  • 分享学习海报图片;
  • 分享 AppLink;
  • 分享某个单词本;
  • 分享复习计划;
  • 分享班级排行榜。

六、分布式 KV:让学习数据跟着设备走

除了分享,学习 App 更重要的是“连续性”。项目里通过 DistributedSync.ets 初始化分布式 KV。

import { distributedKVStore } from '@kit.ArkData'

const STORE_ID = 'yingyu_distributed_store'
let kvStore: distributedKVStore.SingleKVStore | null = null

export async function initDistributedStore(context: Context): Promise<void> {
  const kvManagerConfig: distributedKVStore.KVManagerConfig = {
    bundleName: 'ying.yu.app',
    context: context
  }

  const kvManager = distributedKVStore.createKVManager(kvManagerConfig)
}

存储选项如下:

const options: distributedKVStore.Options = {
  createIfMissing: true,
  encrypt: false,
  backup: false,
  autoSync: true,
  kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
  securityLevel: distributedKVStore.SecurityLevel.S1
}

kvStore = await kvManager.getKVStore<distributedKVStore.SingleKVStore>(
  STORE_ID,
  options
)

这里最重要的是:

  • autoSync: true:数据变更后支持自动同步;
  • SINGLE_VERSION:适合轻量键值数据;
  • SecurityLevel.S1:用于基础学习数据;
  • backup: false:避免不必要的数据备份。

适合同步的数据包括:

  • 已学单词 ID;
  • 每日学习目标;
  • 连续打卡天数;
  • 成就解锁状态;
  • 用户年级设置;
  • 今日复习进度。

不建议同步的数据包括:

  • 大段听力音频;
  • 临时 UI 状态;
  • 敏感身份信息;
  • 可重新计算的缓存数据。

七、跨设备续接:手机学完,平板继续

项目还在 EntryAbility.ets 中实现了 onContinue(),用于跨设备迁移学习状态。

onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
  try {
    const settings = AppStorage.get<string>('yingyu_settings') || ''
    const progress = AppStorage.get<string>('yingyu_progress') || ''
    const learnedWords = AppStorage.get<string>('yingyu_learned_words') || ''

    wantParam['yingyu_settings'] = settings
    wantParam['yingyu_progress'] = progress
    wantParam['yingyu_learned_words'] = learnedWords
    wantParam['yingyu_timestamp'] = Date.now().toString()

    return AbilityConstant.OnContinueResult.AGREE
  } catch (err) {
    return AbilityConstant.OnContinueResult.MISMATCH
  }
}

这个函数做的事情非常明确:把跨设备续接所需的最小数据写入 wantParam

目标设备启动后,通过 onCreateWithWant() 恢复:

onCreateWithWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  this.onCreate(want, launchParam)

  if (want.parameters) {
    const settings = want.parameters['yingyu_settings'] as string
    const progress = want.parameters['yingyu_progress'] as string
    const learnedWords = want.parameters['yingyu_learned_words'] as string

    if (settings) AppStorage.setOrCreate('yingyu_settings', settings)
    if (progress) AppStorage.setOrCreate('yingyu_progress', progress)
    if (learnedWords) AppStorage.setOrCreate('yingyu_learned_words', learnedWords)
  }
}

这样就能实现:

  • 手机上打开词汇学习;
  • 切换到平板;
  • 平板恢复学习设置和已学进度;
  • 用户继续学习,不需要重新选择。

八、全场景体验的产品价值

如果只从技术上看,近场分享、分布式 KV、跨端续接是三个独立能力。但放到英语学习场景里,它们会形成闭环:

  1. 用户在手机上完成今日单词;
  2. 学习进度同步到分布式数据;
  3. 回家后用平板继续练听力;
  4. 学完后碰一碰分享学习报告;
  5. 家长或同学收到报告;
  6. 用户获得反馈和激励。

这就是“全场景”真正有价值的地方:它把学习行为从单点操作变成连续体验。

九、实况窗扩展思路

当前项目还没有完整接入实况窗,但学习场景非常适合做实况窗扩展。可以考虑展示:

  • 今日任务完成度:8/10 个单词;
  • 待复习数量:还有 5 个单词;
  • 听力播放状态:正在播放第 3 条材料;
  • 复习倒计时:下次复习还有 20 分钟;
  • 打卡提醒:今日还未完成目标。

建议后续抽象一个统一状态模型:

interface LearningLiveState {
  taskTitle: string
  current: number
  total: number
  status: 'learning' | 'reviewing' | 'listening' | 'completed'
  updatedAt: number
}

然后从 DailyTaskReviewCenterListeningContent 等页面收集状态,再对接系统级展示。这样实况窗不会变成孤立功能,而是学习流程的自然延伸。

十、实现时需要注意的点

1. 分享监听要跟生命周期绑定

不要在全局长期注册监听。页面出现时注册,离开时注销,逻辑更清楚,也更节省资源。

2. 分享内容要可读

学习报告面向用户,不是面向程序。优先分享自然语言文本,再考虑 JSON 或结构化数据。

3. 同步数据要轻量

分布式 KV 适合轻量状态,不适合大文件。同步学习进度、设置、成就更合适。

4. 续接数据要最小化

跨设备续接只带必要字段,避免把无关数据一起迁移。

5. 实况窗要服务任务

实况窗不应该只是“展示存在感”,而应该帮助用户快速知道当前学习任务的状态。

十一、小结

本文结合「英语视界 YingYu」项目,梳理了 OpenHarmony/HarmonyOS 全场景能力在英语学习 App 中的落地方式:

  • 使用 ShareKit 实现碰一碰和隔空传送;
  • 将学习数据转成可读文本报告;
  • 使用 SharedData 发起近场分享;
  • 使用分布式 KV 存储轻量学习状态;
  • 使用 onContinue()onCreateWithWant() 完成跨设备续接;
  • 基于学习任务扩展实况窗能力。

全场景不是简单地“多设备支持”,而是让用户在不同设备、不同时间、不同场景下都能接着学。对于教育类 App 来说,这种连续性就是体验优势。🌍

img

Logo

一站式 AI 云服务平台

更多推荐