HarmonyOS 6学习:跨页面生命周期的倒计时持久化实战
在HarmonyOS 6应用开发中,倒计时功能是许多应用场景的核心交互,如电商限时抢购、会议预约提醒、运动健身计时等。然而,开发者实现倒计时功能时,经常面临一个棘手的问题:在页面中设置了倒计时,当倒计时还在运行时,用户因切换页面、返回桌面或应用进入后台导致当前页面销毁,再次进入该页面时,倒计时状态丢失,无法正确显示剩余时间。本文将深入剖析HarmonyOS 6中页面生命周期与倒计时状态管理的核心机制,提供基于用户首选项(Preferences)的完整持久化解决方案,确保倒计时能够精准跨越页面生命周期,为用户提供连续、可靠的计时体验。
一、问题场景:页面销毁导致的倒计时“断档”
开发者的实际困境
假设你正在开发一款运动健身应用,其中的“高强度间歇训练(HIIT)”功能需要精确的倒计时:
// 问题代码:简单的页面内倒计时
@Component
struct WorkoutTimerPage {
@State remainingTime: number = 60; // 60秒倒计时
private timerId: number | null = null;
aboutToAppear() {
this.startCountdown();
}
startCountdown() {
this.timerId = setInterval(() => {
if (this.remainingTime > 0) {
this.remainingTime -= 1;
} else {
this.clearTimer();
this.onCountdownComplete();
}
}, 1000);
}
aboutToDisappear() {
this.clearTimer(); // 页面销毁时清除定时器
}
clearTimer() {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
}
}
onCountdownComplete() {
console.log('倒计时结束!');
// 触发训练完成逻辑
}
}
这段代码在单一页面会话中运行良好,但遇到以下场景时会出现问题:
-
场景A:倒计时进行到30秒时,用户接听电话,应用进入后台
-
场景B:倒计时进行到45秒时,用户切换到其他标签页
-
场景C:倒计时进行到20秒时,系统因内存不足回收页面
当用户返回页面时,aboutToAppear()会重新执行,倒计时会从初始值60秒重新开始,而不是从离开时的剩余时间继续。这就是典型的“倒计时断档”问题。
问题本质分析
问题的核心在于页面状态(倒计时数值)与页面生命周期绑定,而定时器任务与页面组件生命周期绑定。当页面销毁时:
-
@State remainingTime被重置 -
定时器被清除
-
所有运行时状态丢失
-
重新进入页面时无法恢复之前的状态
二、技术原理:理解HarmonyOS的页面生命周期与状态管理
1. HarmonyOS页面生命周期详解
在HarmonyOS中,页面的生命周期由以下核心方法构成:
页面生命周期流程图:
创建 → aboutToAppear() → 页面显示
↓
页面活动(用户交互)
↓
aboutToDisappear() → 页面隐藏
↓
可能被销毁 → 可能被重建
↓
aboutToAppear() → 页面重新显示
关键点在于:aboutToDisappear()并不保证页面实例会被销毁,系统可能保留页面实例在后台。但当系统需要回收内存时,页面实例会被销毁,所有状态丢失。
2. 用户首选项(Preferences)持久化机制
用户首选项是HarmonyOS为应用提供的轻量级Key-Value数据存储方案,完美适合存储倒计时相关的状态信息:
|
特性 |
说明 |
适用于倒计时的场景 |
|---|---|---|
|
轻量级 |
适合存储简单数据 |
倒计时起始时间、总时长、状态等 |
|
持久化 |
数据写入文件系统 |
应用关闭后数据不丢失 |
|
异步/同步 |
支持同步和异步操作 |
倒计时需要实时保存状态 |
|
类型安全 |
支持数字、字符串、布尔等类型 |
时间戳用数字类型存储 |
核心API:
-
dataPreferences.getPreferences(): 获取Preferences实例 -
preferences.putSync(): 同步保存数据到缓存 -
preferences.flushSync(): 将缓存数据持久化到文件 ⭐关键步骤 -
preferences.getSync(): 同步读取数据 -
preferences.deleteSync(): 删除数据
3. 倒计时持久化的核心思路
基于链接文档的指导,正确的实现思路是:
倒计时持久化策略:
1. 开始倒计时时:记录“起始时间戳”和“总时长”到Preferences
2. 每次倒计时更新时:可选更新“剩余时间”到Preferences
3. 页面销毁时:自动保存,无需额外操作
4. 页面重建时:
a. 从Preferences读取“起始时间戳”和“总时长”
b. 计算“已过去时间” = 当前时间 - 起始时间戳
c. 计算“剩余时间” = 总时长 - 已过去时间
d. 如果剩余时间 > 0,继续倒计时
e. 如果剩余时间 <= 0,触发倒计时完成
三、完整解决方案:基于Preferences的倒计时管理器
1. Preferences数据管理封装
首先,我们封装一个专门用于管理倒计时数据的Preferences工具类:
// utils/CountdownPreferences.ets
import { dataPreferences } from '@kit.ArkData';
import { BusinessError } from '@ohos.base';
/**
* 倒计时数据持久化管理器
* 使用HarmonyOS用户首选项存储倒计时状态
*/
export class CountdownPreferences {
private static readonly PREF_NAME = 'countdown_preferences';
private static readonly KEY_START_TIME = 'countdown_start_time';
private static readonly KEY_TOTAL_DURATION = 'countdown_total_duration';
private static readonly KEY_IS_RUNNING = 'countdown_is_running';
private static readonly KEY_TAG = 'countdown_tag'; // 用于区分不同倒计时
/**
* 获取Preferences实例
*/
private static async getPreferences(): Promise<dataPreferences.DataPreferences> {
try {
const context = getContext(this) as Context;
return await dataPreferences.getPreferences(context, this.PREF_NAME);
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`获取Preferences失败: ${err.code}, ${err.message}`);
throw error;
}
}
/**
* 保存倒计时开始状态
* @param totalDuration 总时长(秒)
* @param tag 倒计时标识,用于区分多个倒计时
* @returns 保存的起始时间戳
*/
static async saveCountdownStart(totalDuration: number, tag: string = 'default'): Promise<number> {
const preferences = await this.getPreferences();
const startTime = Date.now(); // 当前时间戳
try {
// 保存数据到缓存
preferences.putSync(this.KEY_START_TIME, startTime);
preferences.putSync(this.KEY_TOTAL_DURATION, totalDuration);
preferences.putSync(this.KEY_IS_RUNNING, true);
preferences.putSync(this.KEY_TAG, tag);
// ⭐关键步骤:将缓存数据刷新到持久化文件
// 这是链接文档中特别强调的,putSync后必须调用flushSync
await preferences.flushSync();
console.info(`倒计时开始状态已保存: tag=${tag}, startTime=${startTime}, duration=${totalDuration}s`);
return startTime;
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`保存倒计时状态失败: ${err.code}, ${err.message}`);
throw error;
}
}
/**
* 获取倒计时状态
* @returns 倒计时状态对象,如果不存在返回null
*/
static async getCountdownState(tag: string = 'default'): Promise<CountdownState | null> {
const preferences = await this.getPreferences();
try {
// 检查是否存在指定tag的倒计时
const savedTag = preferences.getSync(this.KEY_TAG, '');
if (savedTag !== tag) {
return null; // 不是同一个倒计时
}
const isRunning = preferences.getSync(this.KEY_IS_RUNNING, false);
if (!isRunning) {
return null; // 倒计时未运行
}
const startTime = preferences.getSync(this.KEY_START_TIME, 0);
const totalDuration = preferences.getSync(this.KEY_TOTAL_DURATION, 0);
if (startTime === 0 || totalDuration === 0) {
return null; // 数据不完整
}
return {
startTime,
totalDuration,
tag: savedTag,
isRunning
};
} catch (error) {
console.error(`读取倒计时状态失败: ${error.message}`);
return null;
}
}
/**
* 计算剩余时间
* @param tag 倒计时标识
* @returns 剩余时间(秒),如果倒计时已结束返回0
*/
static async getRemainingTime(tag: string = 'default'): Promise<number> {
const state = await this.getCountdownState(tag);
if (!state) {
return 0;
}
const now = Date.now();
const elapsedSeconds = Math.floor((now - state.startTime) / 1000);
const remaining = state.totalDuration - elapsedSeconds;
return Math.max(0, remaining); // 确保不为负数
}
/**
* 清除倒计时状态
* @param tag 倒计时标识
*/
static async clearCountdown(tag: string = 'default') {
const preferences = await this.getPreferences();
try {
// 检查是否为同一个倒计时
const savedTag = preferences.getSync(this.KEY_TAG, '');
if (savedTag === tag) {
// 清除所有相关键
preferences.deleteSync(this.KEY_START_TIME);
preferences.deleteSync(this.KEY_TOTAL_DURATION);
preferences.deleteSync(this.KEY_IS_RUNNING);
preferences.deleteSync(this.KEY_TAG);
await preferences.flushSync();
console.info(`倒计时状态已清除: tag=${tag}`);
}
} catch (error) {
console.error(`清除倒计时状态失败: ${error.message}`);
}
}
/**
* 更新倒计时状态为完成
* @param tag 倒计时标识
*/
static async markCountdownComplete(tag: string = 'default') {
const preferences = await this.getPreferences();
try {
const savedTag = preferences.getSync(this.KEY_TAG, '');
if (savedTag === tag) {
preferences.putSync(this.KEY_IS_RUNNING, false);
await preferences.flushSync();
console.info(`倒计时标记为完成: tag=${tag}`);
}
} catch (error) {
console.error(`标记倒计时完成失败: ${error.message}`);
}
}
/**
* 检查倒计时是否应该继续
* 这个方法会在页面重建时调用
* @param tag 倒计时标识
* @returns 如果需要继续倒计时,返回剩余时间;否则返回null
*/
static async shouldResumeCountdown(tag: string = 'default'): Promise<number | null> {
const state = await this.getCountdownState(tag);
if (!state) {
return null;
}
const remaining = await this.getRemainingTime(tag);
if (remaining > 0) {
console.info(`倒计时需要恢复: tag=${tag}, 剩余${remaining}秒`);
return remaining;
} else {
// 倒计时已结束,清理状态
await this.markCountdownComplete(tag);
return null;
}
}
}
// 倒计时状态接口
export interface CountdownState {
startTime: number; // 开始时间戳
totalDuration: number; // 总时长(秒)
tag: string; // 倒计时标识
isRunning: boolean; // 是否正在运行
}
2. 智能倒计时管理器
接下来,创建一个智能的倒计时管理器,它能够处理持久化和恢复逻辑:
// utils/SmartCountdownManager.ets
import { CountdownPreferences } from './CountdownPreferences';
/**
* 智能倒计时管理器
* 支持跨页面生命周期的倒计时持久化
*/
export class SmartCountdownManager {
private totalDuration: number = 0;
private remainingTime: number = 0;
private tag: string = 'default';
private timerId: number | null = null;
private onTickCallback: (remaining: number) => void = () => {};
private onCompleteCallback: () => void = () => {};
private isRunning: boolean = false;
/**
* 开始倒计时
* @param duration 总时长(秒)
* @param tag 倒计时标识
*/
async start(duration: number, tag: string = 'default'): Promise<void> {
this.totalDuration = duration;
this.remainingTime = duration;
this.tag = tag;
// 保存开始状态到持久化存储
await CountdownPreferences.saveCountdownStart(duration, tag);
// 开始计时
this.isRunning = true;
this.startInternalTimer();
console.info(`倒计时开始: ${duration}秒, tag=${tag}`);
}
/**
* 恢复倒计时(页面重建时调用)
* @param tag 倒计时标识
* @returns 是否成功恢复
*/
async resume(tag: string = 'default'): Promise<boolean> {
const remaining = await CountdownPreferences.shouldResumeCountdown(tag);
if (remaining !== null && remaining > 0) {
this.tag = tag;
this.remainingTime = remaining;
this.isRunning = true;
// 计算已过去的时间,用于确定总时长
const state = await CountdownPreferences.getCountdownState(tag);
if (state) {
this.totalDuration = state.totalDuration;
}
this.startInternalTimer();
console.info(`倒计时恢复: 剩余${remaining}秒, tag=${tag}`);
return true;
}
return false;
}
/**
* 暂停倒计时
* 注意:持久化倒计时通常不需要暂停,因为页面销毁会自动"暂停"
* 这里提供暂停功能用于特殊情况
*/
pause(): void {
this.clearInternalTimer();
this.isRunning = false;
console.info(`倒计时暂停: tag=${this.tag}`);
}
/**
* 继续倒计时(从暂停状态恢复)
*/
async continue(): Promise<void> {
if (!this.isRunning && this.remainingTime > 0) {
// 重新保存开始时间,基于当前时间重新计算
const newStartTime = Date.now() - (this.totalDuration - this.remainingTime) * 1000;
// 这里需要直接操作Preferences更新开始时间
// 简化处理:直接调用start,但使用调整后的总时长
await this.start(this.totalDuration, this.tag);
console.info(`倒计时继续: tag=${this.tag}`);
}
}
/**
* 停止倒计时
*/
async stop(): Promise<void> {
this.clearInternalTimer();
this.isRunning = false;
// 清除持久化状态
await CountdownPreferences.clearCountdown(this.tag);
console.info(`倒计时停止: tag=${this.tag}`);
}
/**
* 设置倒计时回调
* @param onTick 每秒回调
* @param onComplete 完成回调
*/
setCallbacks(onTick: (remaining: number) => void, onComplete: () => void): void {
this.onTickCallback = onTick;
this.onCompleteCallback = onComplete;
}
/**
* 获取当前状态
*/
getStatus(): CountdownStatus {
return {
isRunning: this.isRunning,
remainingTime: this.remainingTime,
totalDuration: this.totalDuration,
tag: this.tag,
progress: this.totalDuration > 0 ?
(1 - this.remainingTime / this.totalDuration) : 0
};
}
/**
* 开始内部定时器
*/
private startInternalTimer(): void {
this.clearInternalTimer();
this.timerId = setInterval(() => {
if (this.remainingTime > 0) {
this.remainingTime -= 1;
// 每秒回调
this.onTickCallback(this.remainingTime);
// 每秒更新一次显示时间(可选持久化)
this.optionalPeriodicSave();
} else {
this.handleComplete();
}
}, 1000);
}
/**
* 处理倒计时完成
*/
private async handleComplete(): Promise<void> {
this.clearInternalTimer();
this.isRunning = false;
// 标记为完成
await CountdownPreferences.markCountdownComplete(this.tag);
// 完成回调
this.onCompleteCallback();
console.info(`倒计时完成: tag=${this.tag}`);
}
/**
* 可选的周期性保存
* 对于长时间倒计时,可以定期保存状态防止意外
*/
private optionalPeriodicSave(): void {
// 每10秒保存一次,或根据业务需求调整
if (this.remainingTime % 10 === 0) {
// 这里可以保存当前剩余时间,但注意这会覆盖开始时间逻辑
// 更安全的做法是保持开始时间不变
console.debug(`倒计时心跳: 剩余${this.remainingTime}秒`);
}
}
/**
* 清理内部定时器
*/
private clearInternalTimer(): void {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
}
}
/**
* 销毁资源
*/
async destroy(): Promise<void> {
this.clearInternalTimer();
// 注意:这里不自动清除持久化状态,因为可能需要恢复
}
}
// 倒计时状态接口
export interface CountdownStatus {
isRunning: boolean;
remainingTime: number;
totalDuration: number;
tag: string;
progress: number; // 进度 0-1
}
3. 完整的倒计时页面组件
现在,我们创建一个完整的倒计时页面组件,展示如何集成上述持久化功能:
// view/PersistentCountdownPage.ets
import { SmartCountdownManager, CountdownStatus } from '../utils/SmartCountdownManager';
@Entry
@Component
export struct PersistentCountdownPage {
// 倒计时管理器实例
private countdownManager: SmartCountdownManager = new SmartCountdownManager();
// 页面状态
@State countdownStatus: CountdownStatus = {
isRunning: false,
remainingTime: 0,
totalDuration: 60,
tag: 'workout_timer',
progress: 0
};
@State isInitializing: boolean = true;
@State inputDuration: number = 60;
@State showCompletionDialog: boolean = false;
// 页面生命周期
aboutToAppear() {
this.initializeCountdown();
}
aboutToDisappear() {
// 页面销毁时,定时器会被清除,但状态已持久化
// 这里可以做一些清理工作,但不清理持久化状态
console.info('页面即将消失,倒计时状态已自动持久化');
}
// 初始化倒计时
async initializeCountdown() {
this.isInitializing = true;
// 设置回调
this.countdownManager.setCallbacks(
(remaining: number) => {
// 每秒更新状态
this.updateStatus();
},
() => {
// 倒计时完成
this.onCountdownComplete();
}
);
// 尝试恢复之前的倒计时
const resumed = await this.countdownManager.resume('workout_timer');
if (resumed) {
console.info('成功恢复之前的倒计时');
} else {
console.info('没有可恢复的倒计时,或倒计时已结束');
}
this.updateStatus();
this.isInitializing = false;
}
// 更新状态
updateStatus() {
this.countdownStatus = this.countdownManager.getStatus();
}
// 开始新的倒计时
async startNewCountdown() {
if (this.inputDuration <= 0) {
console.error('倒计时时长必须大于0');
return;
}
// 停止当前的(如果有)
await this.countdownManager.stop();
// 开始新的
await this.countdownManager.start(this.inputDuration, 'workout_timer');
this.updateStatus();
console.info(`开始新的倒计时: ${this.inputDuration}秒`);
}
// 暂停/继续
togglePause() {
if (this.countdownStatus.isRunning) {
this.countdownManager.pause();
} else {
this.countdownManager.continue();
}
this.updateStatus();
}
// 停止倒计时
async stopCountdown() {
await this.countdownManager.stop();
this.updateStatus();
this.showCompletionDialog = false;
}
// 倒计时完成处理
onCountdownComplete() {
this.updateStatus();
this.showCompletionDialog = true;
// 可以添加震动、声音等反馈
console.info('倒计时完成!');
}
// 格式化时间显示 (MM:SS)
formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// 获取进度条颜色
getProgressColor(): ResourceColor {
const progress = this.countdownStatus.progress;
if (!this.countdownStatus.isRunning) {
return '#BDBDBD'; // 未开始/已结束 - 灰色
}
if (progress < 0.3) return '#4CAF50'; // 开始阶段 - 绿色
if (progress < 0.7) return '#FF9800'; // 中间阶段 - 橙色
return '#F44336'; // 最后阶段 - 红色
}
build() {
Column({ space: 30 }) {
// 标题
Text('智能持久化倒计时')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 40, bottom: 20 })
// 倒计时卡片
this.buildCountdownCard()
// 控制面板
this.buildControlPanel()
// 状态信息
this.buildStatusInfo()
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#F5F5F5')
}
@Builder
buildCountdownCard() {
Card() {
Column({ space: 20 }) {
if (this.isInitializing) {
// 初始化中
Column() {
LoadingProgress()
.width(50)
.height(50)
.color(Color.Blue)
Text('检查倒计时状态...')
.fontSize(16)
.fontColor(Color.Gray)
.margin({ top: 12 })
}
.height(200)
.width('100%')
.justifyContent(FlexAlign.Center)
} else {
// 倒计时显示
Column() {
// 大号时间显示
Text(this.formatTime(this.countdownStatus.remainingTime))
.fontSize(64)
.fontWeight(FontWeight.Bold)
.fontColor(this.getProgressColor())
// 进度条
Stack() {
// 背景
Column()
.width('100%')
.height(8)
.backgroundColor('#E0E0E0')
.borderRadius(4)
// 进度
Column()
.width(`${this.countdownStatus.progress * 100}%`)
.height(8)
.backgroundColor(this.getProgressColor())
.borderRadius(4)
}
.width('80%')
.margin({ top: 20 })
// 状态标签
Row() {
if (this.countdownStatus.isRunning) {
Row() {
Circle()
.width(8)
.height(8)
.fill(Color.Red)
.margin({ right: 6 })
Text('进行中')
.fontSize(14)
.fontColor(Color.Red)
}
} else if (this.countdownStatus.remainingTime > 0 && this.countdownStatus.remainingTime < this.countdownStatus.totalDuration) {
Text('已暂停')
.fontSize(14)
.fontColor(Color.Gray)
} else {
Text('未开始')
.fontSize(14)
.fontColor(Color.Gray)
}
}
.margin({ top: 16 })
}
.padding(30)
}
}
}
.width('100%')
.backgroundColor(Color.White)
.shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 })
}
@Builder
buildControlPanel() {
Card() {
Column({ space: 20 }) {
// 时长设置
Column({ space: 8 }) {
Text('设置倒计时时长(秒)')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Start)
Row({ space: 12 }) {
Slider({
value: this.inputDuration,
min: 10,
max: 300,
step: 10
})
.layoutWeight(1)
.onChange((value: number) => {
this.inputDuration = value;
})
Text(`${this.inputDuration}s`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width(60)
}
}
// 控制按钮
Row({ space: 12 }) {
if (!this.countdownStatus.isRunning || this.countdownStatus.remainingTime === 0) {
// 开始按钮
Button('开始倒计时')
.layoutWeight(1)
.height(50)
.fontSize(16)
.backgroundColor('#4CAF50')
.enabled(!this.isInitializing)
.onClick(() => this.startNewCountdown())
} else {
// 暂停/继续按钮
Button(this.countdownStatus.isRunning ? '暂停' : '继续')
.layoutWeight(1)
.height(50)
.fontSize(16)
.backgroundColor('#2196F3')
.onClick(() => this.togglePause())
// 停止按钮
Button('停止')
.layoutWeight(1)
.height(50)
.fontSize(16)
.backgroundColor('#F44336')
.onClick(() => this.stopCountdown())
}
}
}
.padding(20)
}
.width('100%')
.backgroundColor(Color.White)
}
@Builder
buildStatusInfo() {
Column({ space: 12 }) {
Text('持久化状态信息')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Start)
Column({ space: 8 }) {
Row() {
Text('倒计时标识')
.fontSize(14)
.fontColor(Color.Gray)
.layoutWeight(1)
Text(this.countdownStatus.tag)
.fontSize(14)
.fontWeight(FontWeight.Medium)
}
Row() {
Text('总时长')
.fontSize(14)
.fontColor(Color.Gray)
.layoutWeight(1)
Text(`${this.countdownStatus.totalDuration} 秒`)
.fontSize(14)
}
Row() {
Text('当前状态')
.fontSize(14)
.fontColor(Color.Gray)
.layoutWeight(1)
Text(this.countdownStatus.isRunning ? '运行中' :
this.countdownStatus.remainingTime > 0 ? '已暂停' : '未开始/已结束')
.fontSize(14)
.fontColor(this.countdownStatus.isRunning ? Color.Green : Color.Gray)
}
Row() {
Text('持久化支持')
.fontSize(14)
.fontColor(Color.Gray)
.layoutWeight(1)
Text('已启用')
.fontSize(14)
.fontColor(Color.Green)
}
}
Divider()
.margin({ vertical: 12 })
Text('说明:倒计时状态已自动保存,即使退出应用,重新进入后也能恢复剩余时间。')
.fontSize(12)
.fontColor(Color.Gray)
.lineHeight(18)
}
.width('100%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(12)
}
}
4. 完成弹窗组件
// view/CountdownCompleteDialog.ets
@Component
export struct CountdownCompleteDialog {
@Prop showDialog: boolean = false;
@Event onClose: () => void;
@Event onRestart: (duration: number) => void;
@State selectedDuration: number = 60;
build() {
if (this.showDialog) {
// 半透明遮罩
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.position({ x: 0, y: 0 })
.onClick(() => this.onClose())
// 对话框
Column() {
Column({ space: 20 }) {
// 图标
Image($r('app.media.ic_complete'))
.width(80)
.height(80)
.margin({ top: 20 })
// 标题
Text('倒计时完成!')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 消息
Text('您的倒计时已结束。')
.fontSize(16)
.fontColor(Color.Gray)
.textAlign(TextAlign.Center)
// 重新开始选项
Column({ space: 12 }) {
Text('重新开始倒计时')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Start)
Row({ space: 12 }) {
ForEach([30, 60, 90, 120], (duration: number) => {
Button(`${duration}秒`)
.layoutWeight(1)
.padding(12)
.backgroundColor(this.selectedDuration === duration ? '#2196F3' : '#F5F5F5')
.onClick(() => {
this.selectedDuration = duration;
})
})
}
}
.width('100%')
.margin({ top: 10 })
// 按钮
Row({ space: 12 }) {
Button('关闭')
.layoutWeight(1)
.height(50)
.fontSize(16)
.onClick(() => this.onClose())
Button('重新开始')
.layoutWeight(1)
.height(50)
.fontSize(16)
.backgroundColor('#4CAF50')
.onClick(() => {
this.onRestart(this.selectedDuration);
this.onClose();
})
}
.width('100%')
.margin({ top: 10 })
}
.padding(30)
}
.width('80%')
.backgroundColor(Color.White)
.borderRadius(24)
.position({ x: '10%', y: '20%' })
}
}
}
四、使用示例与测试场景
1. 基本使用示例
// 在Ability中集成
@Entry
@Component
struct MainPage {
@State currentPage: string = 'home';
build() {
Column() {
if (this.currentPage === 'home') {
this.buildHomePage();
} else if (this.currentPage === 'countdown') {
PersistentCountdownPage();
}
}
}
@Builder
buildHomePage() {
Column({ space: 20 }) {
Text('智能倒计时演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 60, bottom: 40 })
Button('开始倒计时')
.width('80%')
.height(50)
.fontSize(18)
.backgroundColor('#2196F3')
.onClick(() => {
this.currentPage = 'countdown';
})
Text('功能说明:')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ top: 40 })
.width('80%')
.textAlign(TextAlign.Start)
Text('1. 开始倒计时后,即使退出应用,重新进入也能恢复剩余时间\n' +
'2. 基于HarmonyOS用户首选项实现数据持久化\n' +
'3. 自动计算页面销毁期间的时间差')
.fontSize(14)
.fontColor(Color.Gray)
.lineHeight(20)
.margin({ top: 10 })
.width('80%')
.textAlign(TextAlign.Start)
}
}
}
2. 测试场景验证
可以通过以下场景验证持久化功能:
|
测试场景 |
操作步骤 |
预期结果 |
|---|---|---|
|
正常倒计时 |
开始60秒倒计时,不离开页面 |
正常从60倒数到0 |
|
页面切换恢复 |
倒计时到30秒时切换标签页,5秒后返回 |
显示25秒(30-5) |
|
应用后台恢复 |
倒计时到45秒时按Home键,10秒后返回应用 |
显示35秒(45-10) |
|
应用重启恢复 |
倒计时到20秒时强制停止应用,重新启动 |
显示剩余时间(20-经过时间) |
|
多个倒计时 |
使用不同tag创建多个倒计时 |
各自独立保存和恢复 |
五、最佳实践与注意事项
1. 关键要点总结
-
使用Preferences持久化:保存倒计时起始时间和总时长,而不是剩余时间
-
必须调用flushSync():
putSync()后必须调用flushSync()才能持久化到文件 -
基于时间差计算:恢复时用当前时间减去开始时间,计算已过去的时间
-
标识区分:使用tag区分多个倒计时实例
-
定时器管理:页面销毁时清除定时器,恢复时重新创建
2. 性能优化建议
-
减少持久化频率:只在关键节点(开始、暂停、完成)持久化,避免每秒保存
-
内存管理:页面销毁时及时清理定时器和回调引用
-
错误处理:添加适当的异常处理,防止持久化失败导致应用崩溃
-
数据验证:恢复时验证数据的完整性和合理性
3. 扩展功能建议
-
多倒计时管理:扩展支持多个并发的倒计时
-
云端同步:将倒计时状态同步到云端,实现跨设备继续
-
通知提醒:倒计时结束时发送系统通知
-
历史记录:保存倒计时完成的历史记录
-
自定义样式:支持自定义倒计时显示样式
六、总结
通过本文的完整实现,我们解决了HarmonyOS 6中倒计时功能的跨页面生命周期持久化问题。核心方案是:
-
利用用户首选项持久化:保存倒计时的起始时间戳和总时长
-
时间差计算恢复:通过当前时间与开始时间的差值计算真实剩余时间
-
完整的生命周期管理:在
aboutToAppear()中恢复,在aboutToDisappear()中清理
这个方案不仅适用于倒计时功能,还可以推广到任何需要跨页面生命周期保存状态的场景,如播放进度、游戏状态、阅读位置等。通过合理的持久化策略,可以显著提升应用的用户体验,让应用显得更加智能和可靠。
关键记住:putSync()之后必须调用flushSync(),这是链接文档中特别强调的要点,也是很多开发者容易忽略的关键步骤。只有正确调用flushSync(),数据才能真正持久化到文件系统,在应用重启后依然可用。
更多推荐



所有评论(0)