在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('倒计时结束!');
    // 触发训练完成逻辑
  }
}

这段代码在单一页面会话中运行良好,但遇到以下场景时会出现问题:

  1. 场景A:倒计时进行到30秒时,用户接听电话,应用进入后台

  2. 场景B:倒计时进行到45秒时,用户切换到其他标签页

  3. 场景C:倒计时进行到20秒时,系统因内存不足回收页面

当用户返回页面时,aboutToAppear()会重新执行,倒计时会从初始值60秒重新开始,而不是从离开时的剩余时间继续。这就是典型的“倒计时断档”问题。

问题本质分析

问题的核心在于页面状态(倒计时数值)与页面生命周期绑定,而定时器任务与页面组件生命周期绑定。当页面销毁时:

  1. @State remainingTime被重置

  2. 定时器被清除

  3. 所有运行时状态丢失

  4. 重新进入页面时无法恢复之前的状态

二、技术原理:理解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. 关键要点总结

  1. 使用Preferences持久化:保存倒计时起始时间和总时长,而不是剩余时间

  2. 必须调用flushSync()putSync()后必须调用flushSync()才能持久化到文件

  3. 基于时间差计算:恢复时用当前时间减去开始时间,计算已过去的时间

  4. 标识区分:使用tag区分多个倒计时实例

  5. 定时器管理:页面销毁时清除定时器,恢复时重新创建

2. 性能优化建议

  1. 减少持久化频率:只在关键节点(开始、暂停、完成)持久化,避免每秒保存

  2. 内存管理:页面销毁时及时清理定时器和回调引用

  3. 错误处理:添加适当的异常处理,防止持久化失败导致应用崩溃

  4. 数据验证:恢复时验证数据的完整性和合理性

3. 扩展功能建议

  1. 多倒计时管理:扩展支持多个并发的倒计时

  2. 云端同步:将倒计时状态同步到云端,实现跨设备继续

  3. 通知提醒:倒计时结束时发送系统通知

  4. 历史记录:保存倒计时完成的历史记录

  5. 自定义样式:支持自定义倒计时显示样式

六、总结

通过本文的完整实现,我们解决了HarmonyOS 6中倒计时功能的跨页面生命周期持久化问题。核心方案是:

  1. 利用用户首选项持久化:保存倒计时的起始时间戳和总时长

  2. 时间差计算恢复:通过当前时间与开始时间的差值计算真实剩余时间

  3. 完整的生命周期管理:在aboutToAppear()中恢复,在aboutToDisappear()中清理

这个方案不仅适用于倒计时功能,还可以推广到任何需要跨页面生命周期保存状态的场景,如播放进度、游戏状态、阅读位置等。通过合理的持久化策略,可以显著提升应用的用户体验,让应用显得更加智能和可靠。

关键记住:putSync()之后必须调用flushSync(),这是链接文档中特别强调的要点,也是很多开发者容易忽略的关键步骤。只有正确调用flushSync(),数据才能真正持久化到文件系统,在应用重启后依然可用。

 

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐