HarmonyOS鸿蒙Next中将playList持久化出现的问题

HarmonyOS鸿蒙Next中将playList持久化出现的问题 用到的接口

export interface SongInfo {
  uri: string;
  songName: string;
  duration: number;
  singerName?: string;
  albumCover?: PixelMap;
  MusicList: Map<string, SongInfo>;
  albumName?: string;
  sq?: boolean;
  hq?: boolean;

}
import {  SongInfo } from "../pages/local"

@ObservedV2
export class GlobalMusic {
  @Trace albumCover?:PixelMap | Resource; // 音乐封面
  @Trace songName: string | Resource = ''; // 音乐名称
  @Trace singerName?: string | Resource; // 作者
  @Trace uri: string = '' // 当前播放链接
  @Trace time: number = 0 // 播放时间
  @Trace duration: number = 0 // 音乐的播放时长
  @Trace albumName?:string
  //歌曲列表
  @Trace playIndex: number = 0
  @Trace playList: SongInfo[] = []
  //播放和暂停
  @Trace isPlay: boolean = false //播放的状态+
  //播放模式
  @Trace playMode: 'auto' | 'random' | 'repeat' = 'auto'
  @Trace MusicList: Map<string,  SongInfo> = new Map();
  MediaItem: SongInfo[] = [];

  reset() {
    this.albumCover = undefined
    this.songName = ""
    this.singerName = ""
    this.uri = ""
    this.time = 0
    this.duration = 0
    this.albumName=''
    this.playIndex= 0
    this.playList=[]
    this.isPlay = false
    this.playMode = 'auto'
  }
}

函数

import { GlobalMusic } from "../models/GlobalMusic"
import { AppStorageV2} from "[@kit](/user/kit).ArkUI"
import { playermanager } from "../untils/AVPlayerManager"
import { SongInfo } from '../pages/local';
import PersistPermissionUtils from "../untils/PersistPermissionUtils";


// 跳转页面入口函数
@Builder
export function PlayBuilder() {
  Play()
}

@ComponentV2
struct Play {
  @Local panelHeight: string = '0%'
  @Local panelOpacity: number = 0
  @Local pathStack: NavPathStack = new NavPathStack();
  // 当前播放的歌曲
  @Local playState: GlobalMusic = AppStorageV2.connect(GlobalMusic, 'SONG_KEY', () => new GlobalMusic())!
  aboutToAppear(): void {
    this.playState.playIndex = PersistPermissionUtils.getChangeIndex();

  }

  @Builder
  deleteButton(index: number) {
    Row() {
     
      Button('删除')
        .backgroundColor('#ec5c87')
        .fontColor('#fff')
        .width(80)
        .height('100%')
        .type(ButtonType.Normal)
        .onClick(() => {
          playermanager.remove(index)
          if (this.playState.playList.length === 0) {
            this.pathStack.pop()
          }

        })
    }
  }

  number2time(number: number) {
    // 毫秒 → 秒 → 分+秒; 先判断是否大于1分钟
    if (number > 60 * 1000) {
      const s = Math.floor(number / 1000 % 60)
      const m = Math.floor(number / 1000 / 60)
      const second = s.toString().padStart(2, '0')
      const minute = m.toString().padStart(2, '0')
      return minute + ':' + second
    } else {
      const s = Math.floor(number / 1000 % 60)
      const second = s.toString().padStart(2, '0')
      return '00:' + second
    }
  }

  build() {


    NavDestination() {
      Stack({ alignContent: Alignment.Bottom }) {
        // 播放
        Stack() {
          // 变色背景
          Image( $r('app.media.ic_default'))
            .width('100%')
            .height('100%')
            .blur(300)
            .expandSafeArea()
            // .colorFilter(
            //   { brightness: 0.9 }
            // )
            .backgroundColor('#70000000')
          // 内容
          Column() {
            // //   播放界面
            Column() {
              //顶栏
              Row() {
                Image($r('app.media.down'))
                  .width(30)
                  .height(30)
                  .fillColor(Color.White)
                  .onClick(() => {
                    this.pathStack.pop()
                  })
                Image($r('app.media.more'))
                  .width(30)
                  .height(30)
                  .fillColor(Color.White)
              }
              .justifyContent(FlexAlign.SpaceBetween)
              .width('100%')
              .safeAreaPadding({ top: 40 })
              .padding({left:22,right:22})
              // 图片
              Stack({ alignContent: Alignment.Top }) {
                Row() {
                  Row() {
                    Image(this.playState.playList[this.playState.playIndex].albumCover?? $r('app.media.ic_default'))
                      .width('90%')
                      .borderRadius(20)
                      .shadow({
                        color: Color.Gray,
                        // 阴影颜色
                        radius: 20,
                        // 阴影模糊半径
                        offsetY: 0,
                      })
                  }
                  // .backgroundImage($r('app.media.ic_cd'))
                  .backgroundImageSize(ImageSize.Cover)
                  .justifyContent(FlexAlign.Center)
                  .width('100%')
                  .clip(true)
                  .aspectRatio(1)
                  .safeAreaPadding({top:20})

                }
                .width('100%')
                .aspectRatio(1)
                .justifyContent(FlexAlign.Center)
                .padding(18)

                // 唱针
                // Image($r('app.media.ic_stylus'))
                //   .width(200)
                //   .aspectRatio(1)
                //   .rotate({
                //     angle: -55,
                //     centerX: 100,
                //     centerY: 30
                //   })
                //   .animation({
                //     duration: 500
                //   })

              }


              // 歌曲信息
              Stack() {
                // 第一个
                Column({ space: 8 }) {
                  Text(this.playState.playList[this.playState.playIndex].songName)
                    .fontSize(28)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#4bb0c4')
                  Text(this.playState.playList[this.playState.playIndex].singerName)
                    .fontSize(18)
                    .fontColor('#4bb0c4')
                }
                .layoutWeight(1)
                .justifyContent(FlexAlign.Center)
                .zIndex(1)


              }
              .padding({bottom:10})
              .layoutWeight(1)

              // 操作
              Row() {
                Badge({ value: '', style: { badgeSize: 12, badgeColor: '#00000000', borderWidth: 0 } }) {
                  Image($r("app.media.ic_like"))
                    .fillColor(Color.White)
                    .width(24)
                }

                Badge({ value: '', style: { badgeSize: 12, badgeColor: '#00000000', borderWidth: 0 } }) {
                  Image($r("app.media.ic_comment_o"))
                    .fillColor(Color.White)
                    .width(18)
                }

                Badge({ value: '', style: { badgeSize: 12, badgeColor: '#00000000', borderWidth: 0 } }) {
                  Image($r("app.media.ic_bells_o"))
                    .fillColor(Color.White)
                    .width(24)
                }

                Badge({ value: '', style: { badgeSize: 12, badgeColor: '#00000000', borderWidth: 0 } }) {
                  Image($r("app.media.ic_download_o"))
                    .fillColor(Color.White)
                    .width(24)
                }
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceAround) //平均分布

              // 播放
              Column() {
                // 进度条
                Row({ space: 4 }) {
                  Text(this.number2time(this.playState.time))
                    .fontSize(12)
                    .fontColor(Color.White)
                  Slider({
                    value: this.playState.time,
                    min: 0,
                    max: this.playState.duration
                  })
                    .onChange((value) => {
                      playermanager.seekPlay(value)
                    })
                    .layoutWeight(1)
                    .blockColor(Color.White)
                    .selectedColor(Color.White)
                    .trackColor('#ccc5c5c5')
                    .trackThickness(2)
                  Text(this.number2time(this.playState.playList[this.playState.playIndex].duration))
                    .fontSize(12)
                    .fontColor(Color.White)
                }
                .width('100%')
                .padding(24)

                // 切换
                Row() {
                  if (this.playState.playMode === 'auto') {
                    Image($r('app.media.ic_auto'))
                      .fillColor(Color.White)
                      .width(30)
                      .onClick(() => {
                       playermanager.switchPlayMode()
                      })
                  } else if (this.playState.playMode === 'random') {
                    Image($r('app.media.ic_random'))
                      .fillColor(Color.White)
                      .width(30)
                      .onClick(() => {
                        playermanager.switchPlayMode()
                      })
                  } else if (this.playState.playMode === 'repeat') {
                    Image($r('app.media.ic_repeat'))
                      .fillColor(Color.White)
                      .width(30)
                      .onClick(() => {
                        playermanager.switchPlayMode()
                      })
                  }
                  //上一首
                  Image($r("app.media.ic_prev"))
                    .fillColor(Color.White)
                    .width(30)
                    .onClick(() => {
                      if (!(this.playState.playList.length < 2)) {
                        playermanager.prevPlay()

                      }
                    })
                  // 播放按钮
                  Image(this.playState.isPlay ? $r('app.media.ic_paused') : $r('app.media.ic_play'))
                    .fillColor(Color.White)
                    .width(50)
                    .onClick(() => {
                      this.playState.isPlay?playermanager.pause():playermanager.singplay(this.playState.playList[this.playState.playIndex])
                    })
                  // 下一首
                  Image($r('app.media.ic_next'))
                    .fillColor(Color.White)
                    .width(30)
                    .onClick(() => {
                      if (!(this.playState.playList.length < 2)) {
                        playermanager.nextPlay()
                      PersistPermissionUtils.saveArray(this.playState.playList)
                      }
                    })
                  // 播放列表
                  Image($r('app.media.ic_song_list'))
                    .fillColor(Color.White)
                    .width(30)
                    .onClick(() => {
                      this.panelHeight = '50%'
                      this.panelOpacity = 1
                    })
                }
                .width('100%')
                .padding({
                  bottom: 24
                })
                .justifyContent(FlexAlign.SpaceAround)
              }
              .width('100%')
            }
            .padding({ top: 20, bottom: 20 })
            .layoutWeight(1)
            .width('100%')
          }
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Transparent)


        // 列表
        Column() {
          Column() {
          }
          .height('100%')
          .width('100%')
          .layoutWeight(1)
          .onClick(() => {
            this.panelHeight = '0%'
            this.panelOpacity = 0
          })

          Column() {
            Row() {
              Row() {
                Image($r("app.media.ic_play"))
                  .width(20)
                  .fillColor('#ff5186')
              }
              .width(50)
              .aspectRatio(1)
              .justifyContent(FlexAlign.Center)

              Row({ space: 8 }) {
                Text(`播放列表 (${this.playState.playList.length})`)
                  .fontColor(Color.White)
                  .fontSize(14)
              }
              .layoutWeight(1)


              Image($r('app.media.ic_close'))
                .fillColor('#ffa49a9a')
                .width(24)
                .height(24)
                .margin({ right: 16 })
                .onClick(() => {
                  this.panelHeight = '0%'
                  this.panelOpacity = 0
                })
            }
            .width('100%')
            .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK) // 使用深色背景的模糊样式
            .backgroundColor('start_window_background') // 添加半透明黑色背景增强深色效果
            .padding(8)
            .border({
              width: { bottom: 1 },
              color: '#12ec5c87'
            })
            .borderRadius({
              topLeft: 16,
              topRight: 16
            })

            // 播放列表
            List() {
              ForEach(this.playState.playList, (info: SongInfo, index: number) => {
                ListItem() {
                  Row() {
                    Row() {
                      Text((index + 1).toString())
                        .fontColor('#ffa49a9a')
                    }
                    .width(50)
                    .aspectRatio(1)
                    .justifyContent(FlexAlign.Center)

                    // 列表
                    Row({ space: 10 }) {
                      Column() {
                        Text(info.songName)
                          .fontSize(14)
                          .fontColor('#ffa49a9a')
                          .onClick(() => {
                            playermanager.singplay(info)
                            playermanager.playNewSong(info.uri)
                          })
                        Text(info.singerName)
                          .fontSize(12)
                          .fontColor(Color.Gray)
                          .onClick(() => {
                            playermanager.singplay(info)
                            playermanager.playNewSong(info.uri)
                          })
                      }
                      .layoutWeight(1)
                      .alignItems(HorizontalAlign.Start)
                      .justifyContent(FlexAlign.Center)
                    }
                    .layoutWeight(1)

                    Image($r('app.media.ic_more'))
                      .width(24)
                      .height(24)
                      .margin({ right: 16 })
                      .fillColor(Color.Gray)
                  }
                  .alignItems(VerticalAlign.Center)
                }
                .swipeAction({
                  end: this.deleteButton(index)
                })
                .border({
                  width: { bottom: 1 },
                  color: '#12ec5c87'
                })
              })
            }
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
            .layoutWeight(1)
            .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
            .backgroundColor('start_window_background')
          }
          .height(400)
        }
        // 添加滑动手势处理
        .gesture(
          PanGesture({ direction: PanDirection.Vertical })
            .onActionUpdate((event: GestureEvent) => {
              // 计算垂直滑动距离
              const deltaY = event.offsetY;
              // 根据滑动距离动态调整面板高度
              const newHeight = Math.max(0, 400 - deltaY);
              this.panelHeight = `${(newHeight / 400) * 50}%`;
            })
            .onActionEnd(() => {
              // 滑动结束后判断是否达到关闭阈值
              if (parseFloat(this.panelHeight) < 200) {
                this.panelHeight = '0%';
                this.panelOpacity = 0;
              } else {
                this.panelHeight = '50%';
                this.panelOpacity = 1;
              }
            })
        )
        .height(this.panelHeight)
        .opacity(this.panelOpacity)
        .animation({
          duration: 300,
          curve: Curve.Ease
        })
      }
      .width('100%')
      .height('100%')
      .backgroundColor(Color.Transparent)

    }

    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack
    })
    .onBackPressed(() => {
      if (this.panelOpacity === 1) {
        this.panelHeight = '0%';
        this.panelOpacity = 0;
        return true; // 拦截返回事件
      }
      return false;
    })
    .hideTitleBar(true) // 隐藏标题栏

  }
}

持久化函数

/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the ""License"");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an ""AS IS"" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BusinessError } from '[@kit](/user/kit).BasicServicesKit';
import { fileShare } from '[@kit](/user/kit).CoreFileKit';
import { preferences } from '[@kit](/user/kit).ArkData';
import { SongInfo } from '../pages/local';
import { GlobalMusic } from '../models/GlobalMusic';
import { AppStorageV2 } from '[@kit](/user/kit).ArkUI';


// import Logger from './Logger';
const TAG = 'PersistPermissionUtils';
const KEY_TOGGLE_STATE = 'toggle_state'; // 存储键名
const PREF_NAME = 'MyAppPreferences'; // 首选项名称
const PREFERENCES_NAME = 'array_storage';
const ARRAY_KEY = 'my_array';


class PersistPermissionUtils {
  currentSong: GlobalMusic = AppStorageV2.connect(GlobalMusic, 'SONG_KEY', () => new GlobalMusic())!
  // 用户首选项
  private dataPreferences: preferences.Preferences | null = null
  private context: Context | null = null
  private pref?: preferences.Preferences;
  private preferenceIndex?: preferences.Preferences

更多关于HarmonyOS鸿蒙Next中将playList持久化出现的问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

6 回复

开发者您好,您提供的代码缺少了一个AVPlayerManager,为了便于进一步分析问题,麻烦您提供以下信息:

1.请提供完整的测试demo或者提供下AVPlayerManager,最好是提供下完整的demo;

2.版本信息(DevEco Studio和测试设备的版本信息);

更多关于HarmonyOS鸿蒙Next中将playList持久化出现的问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


好的,回头我弄一个比较简单的demo发给你,

好的,感谢支持!

学到了

在HarmonyOS Next中持久化playList时,常见问题包括:

  1. 数据格式兼容性:使用@ohos.data.preferences@ohos.data.relationalStore时,需确保playList数据结构符合持久化API要求,避免序列化错误。
  2. 存储路径权限:应用需在module.json5中声明ohos.permission.READ_USER_STORAGEohos.permission.WRITE_USER_STORAGE权限。
  3. 数据库版本管理:若使用关系型数据库,升级应用时需通过onUpgrade()处理表结构变更,防止数据丢失或迁移失败。
  4. 异步操作异常:持久化API多为异步调用,需正确处理Promise链,避免因未捕获异常导致数据写入中断。

问题出在 SongInfo 接口中的 albumCover?: PixelMap 字段。PixelMap 是图像的内存对象,无法直接通过 JSON.stringify() 序列化保存到 Preferences 中。

当应用重启后,从持久化数据恢复 playList 时,albumCover 字段会丢失,导致图片无法显示。同时,本地文件的 uri 路径虽然被保存,但对应的文件访问权限在应用重启后失效,导致无法播放。

解决方案:

  1. 修改数据结构,分离可序列化数据SongInfo 拆分为可序列化的元数据和不可序列化的资源对象。

    // 可持久化的歌曲元数据
    export interface SongMeta {
      uri: string;
      songName: string;
      duration: number;
      singerName?: string;
      albumName?: string;
      sq?: boolean;
      hq?: boolean;
      // 改为保存图片资源路径或base64,而不是PixelMap
      albumCoverUri?: string;
    }
    
    // 运行时使用的完整歌曲信息
    export class SongInfo {
      meta: SongMeta;
      albumCover?: PixelMap | Resource; // 运行时根据uri加载
      
      constructor(meta: SongMeta) {
        this.meta = meta;
      }
    }
    
  2. 持久化时只保存元数据 修改 saveArraygetArray 方法,只处理 SongMeta 数组:

    async saveArray(metaArray: SongMeta[]) {
      const prefd = await PersistPermissionUtils.getPreferencesInstance();
      const arrayStr = JSON.stringify(metaArray);
      await prefd.put(ARRAY_KEY, arrayStr);
      await prefd.flush();
      return true;
    }
    
    async getArray(): Promise<SongMeta[]> {
      const prefd = await PersistPermissionUtils.getPreferencesInstance();
      const arrayStr: string = await prefd.get(ARRAY_KEY, '[]') as string;
      const metaArray: SongMeta[] = JSON.parse(arrayStr) as SongMeta[];
      return metaArray;
    }
    
  3. 应用启动时恢复权限并重建播放列表 在应用入口或合适的位置初始化:

    import { getPersistPermissionUtils } from './PersistPermissionUtils';
    import { GlobalMusic } from './models/GlobalMusic';
    import { AppStorageV2 } from '@kit.ArkUI';
    
    // 初始化全局状态
    const globalMusic = AppStorageV2.connect(GlobalMusic, 'SONG_KEY', () => new GlobalMusic())!;
    const persistUtils = getPersistPermissionUtils();
    
    async function restorePlayList() {
      // 1. 读取持久化的元数据
      const metaArray = await persistUtils.getArray();
      
      // 2. 提取所有uri路径,用于权限恢复
      const pathArray = metaArray.map(meta => meta.uri);
      
      // 3. 恢复文件访问权限(关键步骤)
      if (pathArray.length > 0) {
        await persistUtils.activatePermissionExample(pathArray);
      }
      
      // 4. 重建播放列表,将SongMeta转换为SongInfo
      const playList = metaArray.map(meta => {
        const songInfo = new SongInfo(meta);
        // 异步加载图片(示例)
        if (meta.albumCoverUri) {
          // 根据uri加载PixelMap或Resource
          // loadImageAsync(meta.albumCoverUri).then(pixelMap => {
          //   songInfo.albumCover = pixelMap;
          // });
        }
        return songInfo;
      });
      
      // 5. 更新全局状态
      globalMusic.playList = playList;
    }
    
    // 在应用启动时调用
    restorePlayList();
    
  4. 修改保存时机nextPlay 等操作中,保存元数据而非完整对象:

    // 修改点击下一首时的保存逻辑
    PersistPermissionUtils.saveArray(
      this.playState.playList.map(song => song.meta) // 只保存元数据
    );
    

关键点总结:

  • PixelMap 不可序列化,需改为保存资源标识(URI/path/base64)
  • 本地文件访问需要 activatePermission 恢复权限
  • 持久化只保存可序列化的元数据,运行时重新构建完整对象
  • 应用启动时先恢复权限,再加载数据重建播放列表
回到顶部