鸿蒙学习实战之路-Reader Kit阅读进度通知最佳实践
Reader Kit的阅读进度通知功能非常实用,只需要监听pageShow回调,就能实时保存阅读进度,让用户下次打开自动跳转到上次的位置。pageShow回调会在每页渲染完成时触发通过获取当前页的精确位置保存和用于恢复进度下次打开时读取进度并调用startPlay跳转记得调用flush()确保数据写入存储可以按书籍ID保存不同的进度有了这个功能,用户再也不用担心阅读进度丢失了,体验提升不止一个档次
鸿蒙学习实战之路-Reader Kit阅读进度通知最佳实践
朋友们,是不是经常遇到这种情况:用户看了半天书,突然接个电话或者应用被杀,下次打开还得从头开始翻?别慌,Reader Kit提供了阅读进度通知功能,可以实时保存阅读进度,让用户下次打开自动跳转到上次阅读的位置 o(╯□╰)o
今天这篇,我就手把手带你实现阅读进度保存和恢复功能,教你怎么用pageShow回调自动保存进度,全程不超过5分钟(不含数据库配置时间)~
一、为什么需要阅读进度保存?
想象一下场景:
- 用户看了50页书,突然接电话退出应用
- 应用被系统后台清理
- 用户主动退出但没记住看到哪一页
没有进度保存的话,用户下次打开就得从头翻,体验超级差 o(╥﹏╥)o
Reader Kit的阅读进度通知功能,会在每页渲染完成时自动回调,告诉你当前页的精确位置,你只需要把这些信息存到数据库就行了~
二、阅读进度通知的业务流程
整个流程就像这样:
排版引擎渲染完一页
↓
触发pageShow回调
↓
返回页面渲染信息(domPos、resourceIndex等)
↓
将进度信息保存到数据库
↓
用户下次打开时从数据库读取
↓
调用startPlay跳转到上次位置
简单来说,就是"渲染完成 → 自动回调 → 保存进度 → 下次恢复",是不是超简单? (┓( ´∀` )┏
三、需要用到的接口
阅读进度通知主要涉及2个接口:
| 接口名 | 描述 |
|---|---|
| on(‘pageShow’) | 注册页面展示回调,渲染完成时触发 |
| off(‘pageShow’) | 注销页面展示回调,页面销毁时调用 |
核心是PageDataInfo对象,包含了当前页的所有信息:
resourceIndex- 当前章节索引startDomPos- 当前页起始位置totalPageCount- 总页数- 其他元数据…
就像一个精确的书签,告诉用户上次看到哪里了 _
四、开发准备
在开始之前,你需要先按照之前的教程构建一个基本的阅读器,就像要有书才能记录进度一样~
如果没有构建阅读器,可以先去看看"构建阅读器"那篇文章。
另外,你需要准备一个数据库来存储阅读进度,可以选择:
- 用户首选项(Preferences) - 适合简单的键值存储
- 关系型数据库(RelationalStore) - 适合复杂的进度管理
- 键值型数据库(KV-Store) - 适合高性能读写
这里我以用户首选项为例,因为它最简单好用~
五、具体实现步骤
1. 注册pageShow监听器
在阅读页的aboutToAppear生命周期中注册监听器:
import { readerCore } from '@kit.ReaderKit';
import { preferences } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct Reader {
private readerComponentController: readerCore.ReaderComponentController = new readerCore.ReaderComponentController();
private pref: preferences.Preferences | null = null;
aboutToAppear(): void {
this.setOnPageShowListener();
this.initPreferences();
}
/**
* 初始化用户首选项
*/
private async initPreferences() {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
try {
this.pref = await preferences.getPreferences(context, 'reader_progress');
hilog.info(0x0000, 'testTag', 'initPreferences succeeded');
} catch (err) {
hilog.error(0x0000, 'testTag', `initPreferences failed, Code is ${err.code}, message is ${err.message}`);
}
}
/**
* 注册页面展示监听器
*/
private async setOnPageShowListener() {
try {
this.readerComponentController.on('pageShow', (data: readerCore.PageDataInfo): void => {
hilog.info(0x0000, 'testTag', 'pageshow: data is: ' + JSON.stringify(data));
// 在这里保存阅读进度
this.saveReadingProgress(data);
});
} catch (err) {
hilog.error(0x0000, 'testTag', `failed to init, Code is ${err.code}, message is ${err.message}`);
}
}
}
🥦 西兰花小贴士:pageShow回调会在每页渲染完成时触发,翻页、跳转都会触发,所以可以实时保存进度,不怕用户意外退出~
2. 保存阅读进度到数据库
在pageShow回调中保存进度信息:
/**
* 保存阅读进度
*/
private async saveReadingProgress(data: readerCore.PageDataInfo) {
if (!this.pref) {
return;
}
try {
// 构造进度数据
let progressData = {
resourceIndex: data.resourceIndex,
startDomPos: data.startDomPos,
totalPageCount: data.totalPageCount,
currentPageIndex: data.currentPageIndex,
updateTime: new Date().getTime()
};
// 保存到用户首选项
await this.pref.put('reading_progress', JSON.stringify(progressData));
await this.pref.flush();
hilog.info(0x0000, 'testTag', 'saveReadingProgress succeeded');
} catch (err) {
hilog.error(0x0000, 'testTag', `saveReadingProgress failed, Code is ${err.code}, message is ${err.message}`);
}
}
🥦 西兰花警告:
记得在保存完成后调用flush()方法,否则数据可能不会立即写入存储,用户退出后可能会丢失进度 o(╯□╰)o
3. 从数据库读取进度并恢复
在进入阅读页时,读取上次保存的进度并跳转:
/**
* 读取并恢复阅读进度
*/
private async loadAndRestoreProgress(filePath: string) {
if (!this.pref) {
// 没有初始化首选项,从头开始
this.startPlay(filePath, 0, '');
return;
}
try {
// 读取保存的进度
let progressStr = await this.pref.get('reading_progress', '');
if (!progressStr) {
// 没有保存过进度,从头开始
this.startPlay(filePath, 0, '');
return;
}
let progressData = JSON.parse(progressStr as string);
hilog.info(0x0000, 'testTag', 'loadAndRestoreProgress: ' + JSON.stringify(progressData));
// 恢复阅读进度
this.startPlay(
filePath,
progressData.resourceIndex,
progressData.startDomPos
);
} catch (err) {
hilog.error(0x0000, 'testTag', `loadAndRestoreProgress failed, Code is ${err.code}, message is ${err.message}`);
// 读取失败,从头开始
this.startPlay(filePath, 0, '');
}
}
/**
* 打开书籍
*/
private async startPlay(filePath: string, spineIndex: number, domPos: string) {
try {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let initPromise = this.readerComponentController.init(context);
let bookParserHandler = bookParser.getDefaultHandler(filePath);
let result: [bookParser.BookParserHandler, void] = await Promise.all([bookParserHandler, initPromise]);
this.bookParserHandler = result[0];
this.readerComponentController.setPageConfig(this.readerSetting);
this.readerComponentController.registerBookParser(this.bookParserHandler);
this.readerComponentController.startPlay(spineIndex || 0, domPos);
} catch (err) {
hilog.error(0x0000, 'testTag', 'startPlay: err: ' + JSON.stringify(err));
}
}
4. 页面销毁时注销监听器
在aboutToDisappear生命周期中注销监听器:
aboutToDisappear(): void {
this.readerComponentController.off('pageShow');
this.readerComponentController.releaseBook();
}
🥦 西兰花小贴士:
注销监听器是个好习惯,可以避免内存泄漏,虽然鸿蒙系统会在页面销毁时自动清理,但手动注销更保险~
5. 完整示例代码
下面是一个完整的示例,展示如何实现阅读进度保存和恢复:
import { common } from '@kit.AbilityKit';
import { bookParser, ReadPageComponent, readerCore } from '@kit.ReaderKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { display } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { preferences } from '@kit.ArkData';
@Entry
@Component
struct Reader {
private readerComponentController: readerCore.ReaderComponentController = new readerCore.ReaderComponentController();
private bookParserHandler: bookParser.BookParserHandler | null = null;
private pref: preferences.Preferences | null = null;
private readerSetting: readerCore.ReaderSetting = {
fontName: '系统字体',
fontPath: '',
fontSize: 18,
fontColor: '#000000',
fontWeight: 400,
lineHeight: 1.9,
nightMode: false,
themeColor: 'rgba(248, 249, 250, 1)',
themeBgImg: '',
flipMode: '0',
scaledDensity: display.getDefaultDisplaySync().scaledDensity > 0 ? display.getDefaultDisplaySync().scaledDensity : 1,
viewPortWidth: 1260,
viewPortHeight: 2720,
};
@State isLoading: boolean = true;
aboutToAppear(): void {
this.initPreferences().then(() => {
this.setOnPageShowListener();
this.registerPageShowListener();
this.loadAndRestoreProgress();
});
}
/**
* 初始化用户首选项
*/
private async initPreferences() {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
try {
this.pref = await preferences.getPreferences(context, 'reader_progress');
hilog.info(0x0000, 'testTag', 'initPreferences succeeded');
} catch (err) {
hilog.error(0x0000, 'testTag', `initPreferences failed, Code is ${err.code}, message is ${err.message}`);
}
}
/**
* 注册页面展示监听器
*/
private async setOnPageShowListener() {
try {
this.readerComponentController.on('pageShow', (data: readerCore.PageDataInfo): void => {
hilog.info(0x0000, 'testTag', 'pageshow: data is: ' + JSON.stringify(data));
this.saveReadingProgress(data);
});
} catch (err) {
hilog.error(0x0000, 'testTag', `failed to init, Code is ${err.code}, message is ${err.message}`);
}
}
/**
* 注册页面展示监听器(用于控制加载状态)
*/
private registerPageShowListener(): void {
this.readerComponentController.on('pageShow', (data: readerCore.PageDataInfo): void => {
hilog.info(0x0000, 'testTag', 'pageshow: data is: ' + JSON.stringify(data));
if (data.state === readerCore.PageState.PAGE_ON_SHOW) {
this.isLoading = false;
}
});
}
/**
* 保存阅读进度
*/
private async saveReadingProgress(data: readerCore.PageDataInfo) {
if (!this.pref) {
return;
}
try {
let progressData = {
resourceIndex: data.resourceIndex,
startDomPos: data.startDomPos,
totalPageCount: data.totalPageCount,
currentPageIndex: data.currentPageIndex,
updateTime: new Date().getTime()
};
await this.pref.put('reading_progress', JSON.stringify(progressData));
await this.pref.flush();
hilog.info(0x0000, 'testTag', 'saveReadingProgress succeeded');
} catch (err) {
hilog.error(0x0000, 'testTag', `saveReadingProgress failed, Code is ${err.code}, message is ${err.message}`);
}
}
/**
* 读取并恢复阅读进度
*/
private async loadAndRestoreProgress() {
if (!this.pref) {
this.startPlayDefault();
return;
}
try {
let progressStr = await this.pref.get('reading_progress', '');
if (!progressStr) {
this.startPlayDefault();
return;
}
let progressData = JSON.parse(progressStr as string);
hilog.info(0x0000, 'testTag', 'loadAndRestoreProgress: ' + JSON.stringify(progressData));
this.startPlayWithProgress(progressData.resourceIndex, progressData.startDomPos);
} catch (err) {
hilog.error(0x0000, 'testTag', `loadAndRestoreProgress failed, Code is ${err.code}, message is ${err.message}`);
this.startPlayDefault();
}
}
/**
* 从头开始阅读
*/
private async startPlayDefault() {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let filePath: string = `${context.filesDir}/abc.epub`;
this.startPlayWithProgress(0, '');
}
/**
* 从指定进度开始阅读
*/
private async startPlayWithProgress(spineIndex: number, domPos: string) {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let filePath: string = `${context.filesDir}/abc.epub`;
try {
let initPromise = this.readerComponentController.init(context);
let bookParserHandler = bookParser.getDefaultHandler(filePath);
let result: [bookParser.BookParserHandler, void] = await Promise.all([bookParserHandler, initPromise]);
this.bookParserHandler = result[0];
this.readerComponentController.setPageConfig(this.readerSetting);
this.readerComponentController.registerBookParser(this.bookParserHandler);
this.readerComponentController.startPlay(spineIndex || 0, domPos);
} catch (err) {
hilog.error(0x0000, 'testTag', 'startPlay: err: ' + JSON.stringify(err));
}
}
aboutToDisappear(): void {
this.readerComponentController.off('pageShow');
this.readerComponentController.releaseBook();
}
build() {
Stack() {
ReadPageComponent({
controller: this.readerComponentController,
readerCallback: (err: BusinessError, data: readerCore.ReaderComponentController) => {
this.readerComponentController = data;
}
})
Row() {
Text('加载中...')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.visibility(this.isLoading ? Visibility.Visible : Visibility.None)
}.width('100%').height('100%')
}
}
六、进阶优化
1. 按书籍ID保存进度
如果你有多个书籍,可以按书籍ID保存不同的进度:
/**
* 保存阅读进度(按书籍ID)
*/
private async saveReadingProgress(bookId: string, data: readerCore.PageDataInfo) {
if (!this.pref) {
return;
}
try {
let progressData = {
resourceIndex: data.resourceIndex,
startDomPos: data.startDomPos,
totalPageCount: data.totalPageCount,
currentPageIndex: data.currentPageIndex,
updateTime: new Date().getTime()
};
let key = `reading_progress_${bookId}`;
await this.pref.put(key, JSON.stringify(progressData));
await this.pref.flush();
hilog.info(0x0000, 'testTag', 'saveReadingProgress succeeded, key: ' + key);
} catch (err) {
hilog.error(0x0000, 'testTag', `saveReadingProgress failed, Code is ${err.code}, message is ${err.message}`);
}
}
2. 添加阅读进度百分比显示
可以计算阅读进度百分比,显示给用户:
@State readingProgress: number = 0;
private async saveReadingProgress(data: readerCore.PageDataInfo) {
// ... 保存逻辑
// 计算阅读进度百分比
if (data.totalPageCount > 0) {
this.readingProgress = Math.round((data.currentPageIndex / data.totalPageCount) * 100);
}
}
build() {
Stack() {
ReadPageComponent({
controller: this.readerComponentController,
readerCallback: (err: BusinessError, data: readerCore.ReaderComponentController) => {
this.readerComponentController = data;
}
})
// 显示阅读进度
Text(`${this.readingProgress}%`)
.position({ x: '50%', y: '95%'' })
.translate({ x: '-50%', y: '-50%'' })
.fontSize(12)
.fontColor('#999999')
}
}
3. 添加阅读时间统计
记录用户的阅读时间,提供更丰富的数据:
/**
* 保存阅读进度(包含时间统计)
*/
private async saveReadingProgress(bookId: string, data: readerCore.PageDataInfo) {
if (!this.pref) {
return;
}
try {
// 获取之前的进度数据
let progressStr = await this.pref.get(`reading_progress_${bookId}`, '');
let totalTime = 0;
if (progressStr) {
let oldData = JSON.parse(progressStr as string);
totalTime = oldData.totalTime || 0;
}
let progressData = {
resourceIndex: data.resourceIndex,
startDomPos: data.startDomPos,
totalPageCount: data.totalPageCount,
currentPageIndex: data.currentPageIndex,
updateTime: new Date().getTime(),
totalTime: totalTime + 30 // 假设每页阅读30秒
};
await this.pref.put(`reading_progress_${bookId}`, JSON.stringify(progressData));
await this.pref.flush();
} catch (err) {
hilog.error(0x0000, 'testTag', `saveReadingProgress failed, Code is ${err.code}, message is ${err.message}`);
}
}
七、注意事项
🥦 西兰花警告:
-
记得调用flush(): 保存进度后记得调用
flush(),否则数据可能不会立即写入存储 -
处理异常情况: 如果读取进度失败,要给用户一个默认的起点,比如从头开始
-
避免频繁写入: 虽然每页都会触发
pageShow,但实际保存频率可以控制,比如每5页保存一次,避免频繁IO操作 -
多本书籍管理: 如果用户有多本书,记得按书籍ID区分不同的进度
-
进度数据验证: 读取进度时要验证数据的有效性,避免旧数据导致崩溃
八、文档资源
官方文档链接:
九、总结
Reader Kit的阅读进度通知功能非常实用,只需要监听pageShow回调,就能实时保存阅读进度,让用户下次打开自动跳转到上次的位置。
核心要点:
pageShow回调会在每页渲染完成时触发- 通过
PageDataInfo获取当前页的精确位置 - 保存
resourceIndex和startDomPos用于恢复进度 - 下次打开时读取进度并调用
startPlay跳转 - 记得调用
flush()确保数据写入存储 - 可以按书籍ID保存不同的进度
有了这个功能,用户再也不用担心阅读进度丢失了,体验提升不止一个档次! _
下一步行动
建议你:
- 先实现基本的进度保存和恢复功能
- 添加进度百分比显示,让用户知道自己读到哪了
- 支持多本书籍的进度管理
- 添加阅读时间统计,提供更丰富的数据
记住,不教理论,只给你能跑的代码和避坑指南! _
我是盐焗西兰花,
不教理论,只给你能跑的代码和避坑指南。
下期见!🥦
更多推荐

所有评论(0)