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
开发者您好,您提供的代码缺少了一个AVPlayerManager,为了便于进一步分析问题,麻烦您提供以下信息:
1.请提供完整的测试demo或者提供下AVPlayerManager,最好是提供下完整的demo;
2.版本信息(DevEco Studio和测试设备的版本信息);
更多关于HarmonyOS鸿蒙Next中将playList持久化出现的问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
好的,感谢支持!
学到了
在HarmonyOS Next中持久化playList时,常见问题包括:
- 数据格式兼容性:使用
@ohos.data.preferences或@ohos.data.relationalStore时,需确保playList数据结构符合持久化API要求,避免序列化错误。 - 存储路径权限:应用需在
module.json5中声明ohos.permission.READ_USER_STORAGE或ohos.permission.WRITE_USER_STORAGE权限。 - 数据库版本管理:若使用关系型数据库,升级应用时需通过
onUpgrade()处理表结构变更,防止数据丢失或迁移失败。 - 异步操作异常:持久化API多为异步调用,需正确处理Promise链,避免因未捕获异常导致数据写入中断。
问题出在 SongInfo 接口中的 albumCover?: PixelMap 字段。PixelMap 是图像的内存对象,无法直接通过 JSON.stringify() 序列化保存到 Preferences 中。
当应用重启后,从持久化数据恢复 playList 时,albumCover 字段会丢失,导致图片无法显示。同时,本地文件的 uri 路径虽然被保存,但对应的文件访问权限在应用重启后失效,导致无法播放。
解决方案:
-
修改数据结构,分离可序列化数据 将
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; } } -
持久化时只保存元数据 修改
saveArray和getArray方法,只处理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; } -
应用启动时恢复权限并重建播放列表 在应用入口或合适的位置初始化:
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(); -
修改保存时机 在
nextPlay等操作中,保存元数据而非完整对象:// 修改点击下一首时的保存逻辑 PersistPermissionUtils.saveArray( this.playState.playList.map(song => song.meta) // 只保存元数据 );
关键点总结:
PixelMap不可序列化,需改为保存资源标识(URI/path/base64)- 本地文件访问需要
activatePermission恢复权限 - 持久化只保存可序列化的元数据,运行时重新构建完整对象
- 应用启动时先恢复权限,再加载数据重建播放列表


