HarmonyOS NEXT 最近访问记录开发:RecentFile 模型、时间分组与自动记录机制实战
HarmonyOS NEXT 最近访问记录开发:RecentFile 模型、时间分组与自动记录机制实战
前言
文件管理应用中,用户经常需要回顾最近打开过的文件。最近访问记录(Recent Files)功能能够自动追踪用户的文件访问行为,并按时间维度分组展示,帮助用户快速回到上次的工作状态。本文基于 HarmonyExplorer 项目,深入讲解最近访问记录功能的完整开发流程,涵盖 RecentFile 数据模型设计、访问记录自动记录、Preferences 持久化、时间分组展示、去重与数量限制、清除历史记录等核心技术与实现细节。通过本文的学习,开发者可以掌握鸿蒙原生访问记录系统的设计模式。
最近访问记录功能是文件管理应用的标配功能,涉及数据采集、持久化存储和分组展示三大核心环节,参考 HarmonyOS 数据管理开发指南。
一、最近访问功能概述
1.1 功能背景
在日常文件管理场景中,用户可能同时处理多个文件,频繁在目录间切换。如果没有访问记录功能,用户需要记住文件路径并手动导航,效率低下。最近访问功能自动记录用户打开的文件,并提供按时间分组的快速访问入口。
1.2 设计目标
最近访问功能的设计目标如下:
| 设计目标 | 说明 |
|---|---|
| 自动记录 | 用户打开文件时自动记录,无需手动操作 |
| 时间分组 | 按今天、昨天、更早三个时间段分组展示 |
| 去重处理 | 同一文件多次打开只保留最近一次记录 |
| 数量限制 | 最多保留 100 条记录,防止无限增长 |
| 一键清除 | 支持清空全部访问历史记录 |
二、RecentFile 数据模型设计
2.1 数据结构定义
RecentFile 模型记录文件访问的核心信息,采用路径索引设计,确保同一文件路径只保留一条记录。
// model/RecentFile.ets
export interface RecentFile {
id: string;
path: string;
lastOpenTime: number;
}
export interface RecentGroup {
title: string;
items: Array<RecentItem>;
}
export interface RecentItem {
recent: RecentFile;
fileInfo: FileInfo;
}
export enum TimeGroupType {
TODAY = '今天',
YESTERDAY = '昨天',
EARLIER = '更早'
}
2.2 模型字段说明
RecentFile 模型字段设计如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 访问记录唯一标识 |
| path | string | 文件完整路径,作为去重依据 |
| lastOpenTime | number | 最后访问时间戳,用于排序和分组 |
提示:RecentFile 模型仅存储路径和访问时间,文件详情通过 FileUtil 动态查询,保证数据一致性,参考 HarmonyOS 文件系统开发文档。
三、Preferences 持久化方案
3.1 存储结构设计
最近访问记录使用 Preferences 进行持久化存储。与收藏夹类似,采用 JSON 序列化方式将记录列表存储为字符串值。不同之处在于最近记录需要频繁更新,因此引入内存缓存机制减少 IO 操作。
3.2 PreferenceUtil 扩展
// utils/PreferenceUtil.ets 中的最近记录相关方法
const RECENT_KEY: string = 'recent_file_list';
const MAX_RECENT_COUNT: number = 100;
export class PreferenceUtil {
public static async saveRecentFiles(recentFiles: Array<RecentFile>): Promise<void> {
if (this.preferenceInstance === null) {
return;
}
const limited: Array<RecentFile> = recentFiles.slice(0, MAX_RECENT_COUNT);
const jsonStr: string = JSON.stringify(limited);
await this.preferenceInstance.put(RECENT_KEY, jsonStr);
await this.preferenceInstance.flush();
}
public static async loadRecentFiles(): Promise<Array<RecentFile>> {
if (this.preferenceInstance === null) {
return [];
}
const value: dataPreferences.ValueType = await this.preferenceInstance.get(RECENT_KEY, '');
if (typeof value !== 'string' || value.length === 0) {
return [];
}
const parsed: Array<RecentFile> = JSON.parse(value);
return parsed;
}
public static async clearRecentFiles(): Promise<void> {
if (this.preferenceInstance === null) {
return;
}
await this.preferenceInstance.delete(RECENT_KEY);
await this.preferenceInstance.flush();
}
}
四、Recent Repository 层实现
4.1 Repository 接口设计
RecentRepository 负责访问记录的增删查改,核心逻辑包括去重、排序和数量限制。
// repository/RecentRepository.ets
export class RecentRepository {
private recentFiles: Array<RecentFile> = [];
private readonly maxCount: number = 100;
public async loadAll(): Promise<Array<RecentFile>> {
this.recentFiles = await PreferenceUtil.loadRecentFiles();
return this.recentFiles;
}
public async addRecord(path: string): Promise<void> {
this.removeDuplicate(path);
const record: RecentFile = {
id: this.generateId(),
path: path,
lastOpenTime: Date.now()
};
this.recentFiles.unshift(record);
this.recentFiles = this.recentFiles.slice(0, this.maxCount);
await PreferenceUtil.saveRecentFiles(this.recentFiles);
}
public async removeRecord(path: string): Promise<void> {
this.recentFiles = this.recentFiles.filter((item: RecentFile): boolean => {
return item.path !== path;
});
await PreferenceUtil.saveRecentFiles(this.recentFiles);
}
public async clearAll(): Promise<void> {
this.recentFiles = [];
await PreferenceUtil.clearRecentFiles();
}
private removeDuplicate(path: string): void {
this.recentFiles = this.recentFiles.filter((item: RecentFile): boolean => {
return item.path !== path;
});
}
private generateId(): string {
return 'recent_' + Date.now().toString() + Math.floor(Math.random() * 1000).toString();
}
}
4.2 去重与数量限制逻辑
- 去重处理:每次添加记录前,先移除同路径的旧记录,再插入新记录到列表头部,确保最新访问排在最前
- 数量限制:添加记录后截取前 100 条,超出部分自动丢弃,防止 Preferences 存储无限增长
五、Recent ViewModel 实现
5.1 ViewModel 状态管理
RecentViewModel 管理 @Observed 响应式状态,包括分组列表、加载状态和清除操作。
// viewmodel/RecentViewModel.ets
@Observed
export class RecentViewModel extends ObservedObject {
public groups: Array<RecentGroup> = [];
public isLoading: boolean = false;
public totalCount: number = 0;
private repository: RecentRepository = new RecentRepository();
public async loadRecentFiles(): Promise<void> {
this.isLoading = true;
const recentFiles: Array<RecentFile> = await this.repository.loadAll();
const items: Array<RecentItem> = await this.buildRecentItems(recentFiles);
this.groups = this.buildTimeGroups(items);
this.totalCount = recentFiles.length;
this.isLoading = false;
}
public async addRecentRecord(path: string): Promise<void> {
await this.repository.addRecord(path);
await this.loadRecentFiles();
}
public async clearAllRecords(): Promise<void> {
await this.repository.clearAll();
this.groups = [];
this.totalCount = 0;
}
private async buildRecentItems(recentFiles: Array<RecentFile>): Promise<Array<RecentItem>> {
const items: Array<RecentItem> = [];
for (const recent of recentFiles) {
const fileInfo: FileInfo | null = await FileUtil.getFileInfo(recent.path);
if (fileInfo !== null) {
items.push({ recent: recent, fileInfo: fileInfo });
}
}
return items;
}
}
5.2 时间分组逻辑
时间分组是最近访问功能的核心特色,将访问记录按时间维度分为三组展示:
- 今天:当天 00:00 至当前时间访问的文件
- 昨天:前一天 00:00 至当天 00:00 访问的文件
- 更早:前一天 00:00 之前访问的文件
// viewmodel/RecentViewModel.ets 中的分组方法
private buildTimeGroups(items: Array<RecentItem>): Array<RecentGroup> {
const groups: Array<RecentGroup> = [];
const todayItems: Array<RecentItem> = [];
const yesterdayItems: Array<RecentItem> = [];
const earlierItems: Array<RecentItem> = [];
const now: Date = new Date();
const todayStart: number = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const yesterdayStart: number = todayStart - 86400000;
for (const item of items) {
if (item.recent.lastOpenTime >= todayStart) {
todayItems.push(item);
} else if (item.recent.lastOpenTime >= yesterdayStart) {
yesterdayItems.push(item);
} else {
earlierItems.push(item);
}
}
if (todayItems.length > 0) {
groups.push({ title: TimeGroupType.TODAY, items: todayItems });
}
if (yesterdayItems.length > 0) {
groups.push({ title: TimeGroupType.YESTERDAY, items: yesterdayItems });
}
if (earlierItems.length > 0) {
groups.push({ title: TimeGroupType.EARLIER, items: earlierItems });
}
return groups;
}
六、访问记录自动记录机制
6.1 自动记录触发点
访问记录的自动记录需要在用户打开文件的关键节点触发。HarmonyExplorer 在以下场景自动记录:
- 用户在文件列表中点击文件项
- 用户在搜索结果中打开文件
- 用户在收藏列表中打开文件
- 用户通过外部链接打开文件
6.2 记录触发实现
// service/FileOpenService.ets
export class FileOpenService {
private recentViewModel: RecentViewModel = new RecentViewModel();
public async openFile(fileInfo: FileInfo): Promise<void> {
await this.recentViewModel.addRecentRecord(fileInfo.path);
await this.navigateToViewer(fileInfo);
EventBus.emit('recent_file_opened', { path: fileInfo.path });
}
private async navigateToViewer(fileInfo: FileInfo): Promise<void> {
const route: string = this.getRouteByType(fileInfo.type);
await RouterUtil.push(route, { path: fileInfo.path });
}
private getRouteByType(type: FileType): string {
switch (type) {
case FileType.IMAGE:
return 'pages/ImageViewerPage';
case FileType.VIDEO:
return 'pages/VideoPlayerPage';
case FileType.AUDIO:
return 'pages/AudioPlayerPage';
case FileType.PDF:
return 'pages/PdfViewerPage';
case FileType.TEXT:
return 'pages/TextViewerPage';
default:
return 'pages/FileDetailPage';
}
}
}
提示:自动记录逻辑应放在统一的 Service 层,而非分散在各个页面中,确保记录行为的一致性和可维护性,参考 HarmonyOS 架构设计指南。
七、最近访问页面 UI 实现
7.1 页面布局设计
最近访问页面采用分组列表布局,每个时间分组使用 SectionTitle 组件作为标题,下方为文件列表。

图为最近访问页面 UI 布局效果,按时间分组展示文件列表,顶部导航栏包含清除按钮。
7.2 页面代码实现
// pages/RecentPage.ets
@Entry
@Component
struct RecentPage {
@State viewModel: RecentViewModel = new RecentViewModel();
@State showClearDialog: boolean = false;
async aboutToAppear(): Promise<void> {
await this.viewModel.loadRecentFiles();
EventBus.on('recent_file_opened', (data: Object): void => {
this.viewModel.loadRecentFiles();
});
}
build(): void {
Column() {
AppNavigationBar({
title: '最近访问',
showBack: true,
rightAction: (): void => { this.showClearDialog = true; }
})
if (this.viewModel.isLoading) {
LoadingView()
} else if (this.viewModel.totalCount === 0) {
EmptyView({ message: '暂无访问记录', icon: $r('app.media.empty_recent') })
} else {
List({ space: 12 }) {
ForEach(this.viewModel.groups, (group: RecentGroup): void => {
ListItem() {
Column() {
SectionTitle({ title: group.title })
ForEach(group.items, (item: RecentItem): void => {
FileCard({
fileInfo: item.fileInfo,
onItemClick: (): Promise<void> => this.openFile(item.fileInfo)
})
}, (item: RecentItem): string => item.recent.id)
}
}
}, (group: RecentGroup): string => group.title)
}
.layoutWeight(1)
}
}
}
private async openFile(fileInfo: FileInfo): Promise<void> {
await FileOpenService.getInstance().openFile(fileInfo);
}
}
八、时间分组展示组件
8.1 SectionTitle 组件
时间分组标题使用 SectionTitle 组件统一渲染,保持视觉风格一致性。
// components/SectionTitle.ets
@Component
export struct SectionTitle {
private title: string = '';
build(): void {
Text(this.title)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.text_secondary'))
.margin({ top: 8, bottom: 4, left: 16 })
}
}
8.2 分组展示效果
分组展示效果对比如下:
| 分组 | 时间范围 | 排序方式 |
|---|---|---|
| 今天 | 当天 00:00 至当前时间 | 按访问时间降序 |
| 昨天 | 前一天 00:00 至当天 00:00 | 按访问时间降序 |
| 更早 | 前一天 00:00 之前 | 按访问时间降序 |
九、清除历史记录功能
9.1 清除确认弹窗
清除历史记录是不可逆操作,需要通过 ConfirmDialog 组件进行二次确认,防止用户误操作。
// components/ConfirmDialog.ets
@Component
export struct ConfirmDialog {
private title: string = '确认清除';
private message: string = '确定要清除全部访问记录吗?此操作不可撤销。';
private onConfirm: () => void = (): void => {};
@State isVisible: boolean = false;
build(): void {
if (this.isVisible) {
Column() {
Text(this.title).fontSize(18).fontWeight(FontWeight.Bold).margin({ bottom: 12 })
Text(this.message).fontSize(14).fontColor($r('app.color.text_secondary')).margin({ bottom: 20 })
Row({ space: 12 }) {
Button('取消').layoutWeight(1).onClick((): void => { this.isVisible = false; })
Button('确认清除').layoutWeight(1).type(ButtonType.Capsule)
.backgroundColor($r('app.color.danger'))
.onClick((): void => { this.isVisible = false; this.onConfirm(); })
}
}.padding(24).backgroundColor(Color.White).borderRadius(16)
}
}
}
9.2 清除逻辑实现
// pages/RecentPage.ets 中的清除逻辑
private async handleClearConfirm(): Promise<void> {
await this.viewModel.clearAllRecords();
ToastUtil.show('已清除全部访问记录');
this.showClearDialog = false;
}
十、最近访问与文件详情联动
10.1 联动设计方案
最近访问功能与文件详情页的联动通过 EventBus 事件机制实现。当用户从最近列表打开文件后,文件详情页的访问会反向通知最近列表刷新数据。
10.2 联动实现
// pages/RecentPage.ets 中的事件监听
interface FileOpenedEventData {
path: string;
}
async aboutToAppear(): Promise<void> {
EventBus.on('file_opened', (data: FileOpenedEventData): void => {
this.viewModel.addRecentRecord(data.path);
});
await this.viewModel.loadRecentFiles();
}
async aboutToDisappear(): Promise<void> {
EventBus.off('file_opened');
}
提示:在页面销毁时必须注销事件监听,否则会导致内存泄漏和重复触发问题,参考 HarmonyOS 页面生命周期管理。
总结
本文详细介绍了 HarmonyExplorer 最近访问记录功能的完整实现方案,从 RecentFile 数据模型设计到 Preferences 持久化,从时间分组算法到自动记录机制,覆盖了访问记录系统的全流程开发。通过 Repository 分层、去重与数量限制策略、EventBus 联动机制,实现了高效可靠的访问记录管理。开发者可以将此设计模式应用于历史记录、操作日志等类似场景。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源
更多推荐



所有评论(0)