文章的目的为了记录使用Arkts 进行Harmony app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 Arkts 鸿蒙应用 开发(一)工程文件分析-CSDN博客
开源 Arkts 鸿蒙应用 开发(二)封装库.har制作和应用-CSDN博客
开源 Arkts 鸿蒙应用 开发(三)Arkts的介绍-CSDN博客
开源 Arkts 鸿蒙应用 开发(四)布局和常用控件-CSDN博客
开源 Arkts 鸿蒙应用 开发(五)控件组成和复杂控件-CSDN博客
开源 Arkts 鸿蒙应用 开发(六)数据持久--文件和首选项存储-CSDN博客
开源 Arkts 鸿蒙应用 开发(七)数据持久--sqlite关系数据库-CSDN博客
开源 Arkts 鸿蒙应用 开发(八)多媒体--相册和相机-CSDN博客
开源 Arkts 鸿蒙应用 开发(九)通讯--tcp客户端-CSDN博客
开源 Arkts 鸿蒙应用 开发(十)通讯--Http-CSDN博客
开源 Arkts 鸿蒙应用 开发(十一)证书和包名修改-CSDN博客
开源 Arkts 鸿蒙应用 开发(十二)传感器的使用-CSDN博客
推荐链接:
开源 java android app 开发(一)开发环境的搭建-CSDN博客
开源 java android app 开发(二)工程文件结构-CSDN博客
开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客
开源 java android app 开发(四)GUI界面重要组件-CSDN博客
开源 java android app 开发(五)文件和数据库存储-CSDN博客
开源 java android app 开发(六)多媒体使用-CSDN博客
开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客
开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客
开源 java android app 开发(九)后台之线程和服务-CSDN博客
开源 java android app 开发(十)广播机制-CSDN博客
开源 java android app 开发(十一)调试、发布-CSDN博客
开源 java android app 开发(十二)封库.aar-CSDN博客
推荐链接:
开源C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客
开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客
开源 C# .net mvc 开发(三)WEB内外网访问(VS发布、IIS配置网站、花生壳外网穿刺访问)_c# mvc 域名下不可訪問內網,內網下可以訪問域名-CSDN博客
开源 C# .net mvc 开发(四)工程结构、页面提交以及显示_c#工程结构-CSDN博客
开源 Arkts 鸿蒙应用 开发(十)通讯--Http数据传输-CSDN博客开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客
本章内容主HarmonyOS next 系统上的本机音频怎么播放,实现了一个简易的音乐播放器,可以播放App资源文件夹下的3首mp3。
1.代码结构分析
2.文件分析说明
3.显示效果
一、代码结构分析
需要添加和修改的文件如下:
entry/src/main/ets/pages 文件夹下需要有App.ets,Index.et,List.ets。
entry/src/main/resoures/base/profile/main_pages.json 页面配置文件也需要修改
entry/src/main/resoures/rawfile 文件加下需要有3个.mp3文件
二、文件分析说明
2.1 List.ets显示了音乐的列表,选择以后,跳转到Index.ets页面播放相应的音乐
以下为 List.ets代码
import { router } from '@kit.ArkUI';
import storage from './App';
@Entry(storage)
@Component
struct Index {
@LocalStorageLink('myData') myData: string = 'test_01';
// 列表数据 - 字符串数组
private dataList: string[] = [
"test_01",
"test_02",
"test_03",
];
build() {
Column() {
// 列表标题
Text('音乐列表')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 10 })
// ListView 实现
List({ space: 10 }) {
ForEach(this.dataList, (item: string) => {
ListItem() {
Text(item)
.fontSize(18)
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
.backgroundColor(Color.White)
.borderRadius(8)
}
.onClick(() => {
this.myData = item.toString();
console.log("myData1",this.myData);
// 点击跳转到详情页,传递当前项内容
router.push({
url: 'pages/Index',
});
})
})
}
.width('100%')
.layoutWeight(1) // 占据剩余空间
.margin(10)
.divider({
strokeWidth: 1,
color: '#EEEEEE',
startMargin: 20,
endMargin: 20
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
2.2 Index.ets 文件,通过AVPlayer可以实现端到端播放原始媒体资源。
1)播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。
播放状态变化示意图
2)函数分析
AVPlayer封装
使用@ohos.multimedia.media模块的AVPlayer实现媒体播放
提供play()、pause()、seek()、setSpeed()等基本控制方法
通过fdSrc从资源文件加载媒体
状态管理:
监听stateChange事件处理播放器状态(idle/initialized/prepared/playing等)
播放器初始化
async avSetupAudio() {
// 获取资源文件描述符
let fileDescriptor = await this.context.resourceManager.getRawFd(this.fileName);
let avFileDescriptor = { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
// 创建AVPlayer实例
this.avPlayer = await media.createAVPlayer();
// 设置回调
await this.setAVPlayerCallback(...);
// 设置播放源
this.avPlayer.fdSrc = avFileDescriptor;
}
状态回调处理
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'initialized':
// 设置Surface并准备播放
this.avPlayer.surfaceId = this.surfaceId;
this.avPlayer.prepare();
break;
case 'prepared':
// 开始播放并设置初始倍速
this.avPlayer.play();
break;
case 'playing':
// 更新UI状态
break;
// 其他状态处理...
}
});
3)以下为Index.ets代码
/*
* Copyright (c) 2023-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 display from '@ohos.display';
import emitter from '@ohos.events.emitter';
import { common } from '@kit.AbilityKit';
import media from '@ohos.multimedia.media';
import { router } from '@kit.ArkUI';
import storage from './App';
const PROPORTION = 0.99; // 占屏幕比例
const SURFACE_W = 0.9; // 表面宽比例
const SURFACE_H = 1.78; // 表面高比例
const SET_INTERVAL = 100; // interval间隔时间
const TIME_ONE = 60000;
const TIME_TWO = 1000;
const SPEED_ZERO = 0;
const SPEED_ONE = 1;
const SPEED_TWO = 2;
const SPEED_THREE = 3;
const SPEED_COUNT = 4;
let innerEventFalse: emitter.InnerEvent = {
eventId: 1,
priority: emitter.EventPriority.HIGH
};
let innerEventTrue: emitter.InnerEvent = {
eventId: 2,
priority: emitter.EventPriority.HIGH
};
let innerEventWH: emitter.InnerEvent = {
eventId: 3,
priority: emitter.EventPriority.HIGH
};
@Entry(storage)
@Component
struct Index {
@LocalStorageLink('myData') myData: string = '';
tag: string = 'AVPlayManager';
private xComponentController: XComponentController = new XComponentController();
private avPlayer: media.AVPlayer | null = null;
private surfaceId: string = '';
private intervalID: number = -1;
private seekTime: number = -1;
private context: common.UIAbilityContext | undefined = undefined;
private count: number = 0;
@State fileName: string = 'test_01.mp3';
@State isSwiping: boolean = false; // 用户滑动过程中
@State isPaused: boolean = true; // 暂停播放
@State XComponentFlag: boolean = false;
@State speedSelect: number = 0; // 倍速选择
@State speedList: Resource[] = [$r('app.string.video_speed_1_0X'), $r('app.string.video_speed_1_25X'), $r('app.string.video_speed_1_75X'), $r('app.string.video_speed_2_0X')];
@StorageLink('durationTime') durationTime: number = 0; // 视频总时长
@StorageLink('currentTime') currentTime: number = 0; // 视频当前时间
@StorageLink('speedName') speedName: Resource = $r('app.string.video_speed_1_0X');
@StorageLink('speedIndex') speedIndex: number = 0; // 倍速索引
@State surfaceW: number | null = null;
@State surfaceH: number | null = null;
@State percent: number = 0;
@State windowWidth: number = 300;
@State windowHeight: number = 200;
getDurationTime(): number {
return this.durationTime;
}
getCurrentTime(): number {
return this.currentTime;
}
timeConvert(time: number): string {
let min: number = Math.floor(time / TIME_ONE);
let second: string = ((time % TIME_ONE) / TIME_TWO).toFixed(0);
// return `${min}:${(+second < TIME_THREE ? '0' : '') + second}`;
second = second.padStart(2, '0');
return `${min}:${second}`;
}
async msleepAsync(ms: number): Promise<boolean> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true)
}, ms)
})
}
async avSetupAudio() {
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址。
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度。
if (this.context == undefined) return;
let fileDescriptor = await this.context.resourceManager.getRawFd(this.fileName);
let avFileDescriptor: media.AVFileDescriptor =
{ fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
if (this.avPlayer) {
console.info(`${this.tag}: init avPlayer release2createNew`);
this.avPlayer.release();
await this.msleepAsync(1500);
}
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {
this.percent = avPlayer.width / avPlayer.height;
this.setVideoWH();
this.durationTime = this.getDurationTime();
setInterval(() => { // 更新当前时间
if (!this.isSwiping) {
this.currentTime = this.getCurrentTime();
}
}, SET_INTERVAL);
});
// 为fdSrc赋值触发initialized状态机上报
this.avPlayer.fdSrc = avFileDescriptor;
}
avPlay(): void {
if (this.avPlayer) {
try {
this.avPlayer.play();
} catch (e) {
console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);
}
}
}
avPause(): void {
if (this.avPlayer) {
try {
this.avPlayer.pause();
console.info(`${this.tag}: avPause==`);
} catch (e) {
console.error(`${this.tag}: avPause== ${JSON.stringify(e)}`);
}
}
}
async avSeek(seekTime: number, mode: SliderChangeMode): Promise<void> {
if (this.avPlayer) {
try {
console.info(`${this.tag}: videoSeek seekTime== ${seekTime}`);
this.avPlayer.seek(seekTime, 2);
this.currentTime = seekTime;
} catch (e) {
console.error(`${this.tag}: videoSeek== ${JSON.stringify(e)}`);
}
}
}
avSetSpeed(speed: number): void {
if (this.avPlayer) {
try {
this.avPlayer.setSpeed(speed);
console.info(`${this.tag}: avSetSpeed enum ${speed}`);
} catch (e) {
console.error(`${this.tag}: avSetSpeed == ${JSON.stringify(e)}`);
}
}
}
// 注册avplayer回调函数
async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void, vType?: number): Promise<void> {
// seek操作结果回调函数
if (this.avPlayer == null) {
console.error(`${this.tag}: avPlayer has not init!`);
return;
}
this.avPlayer.on('seekDone', (seekDoneTime) => {
console.info(`${this.tag}: setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
});
this.avPlayer.on('speedDone', (speed) => {
console.info(`${this.tag}: setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);
});
// error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
this.avPlayer.on('error', (err) => {
console.error(`${this.tag}: setAVPlayerCallback Invoke avPlayer failed ${JSON.stringify(err)}`);
if (this.avPlayer == null) {
console.error(`${this.tag}: avPlayer has not init on error`);
return;
}
this.avPlayer.reset();
});
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
if (this.avPlayer == null) {
console.info(`${this.tag}: avPlayer has not init on state change`);
return;
}
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state idle called.`);
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state initialized called.`);
if (this.surfaceId) {
this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置
console.info(`${this.tag}: setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);
this.avPlayer.prepare();
}
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state prepared called.`);
this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
console.info(`${this.tag}: bufferingUpdate called, infoType value: ${infoType}, value:${value}}`);
})
this.durationTime = this.avPlayer.duration;
this.currentTime = this.avPlayer.currentTime;
this.avPlayer.play(); // 调用播放接口开始播放
console.info(`${this.tag}:
setAVPlayerCallback speedSelect: ${this.speedSelect}, duration: ${this.durationTime}`);
if (this.speedSelect != -1) {
switch (this.speedSelect) {
case SPEED_ZERO:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
break;
case SPEED_ONE:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
break;
case SPEED_TWO:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
break;
case SPEED_THREE:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
break;
}
}
callback(this.avPlayer);
break;
case 'playing': // play成功调用后触发该状态机上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state playing called.`);
if (this.count !== 0) {
if (this.intervalID != -1) {
clearInterval(this.intervalID)
}
this.intervalID = setInterval(() => { // 更新当前时间
AppStorage.setOrCreate('durationTime', this.durationTime);
AppStorage.setOrCreate('currentTime', this.currentTime);
}, 100);
let eventDataTrue: emitter.EventData = {
data: {
'flag': true
}
};
let innerEventTrue: emitter.InnerEvent = {
eventId: 2,
priority: emitter.EventPriority.HIGH
};
emitter.emit(innerEventTrue, eventDataTrue);
} else {
setTimeout(() => {
console.info('AVPlayer playing wait to pause');
this.avPlayer?.pause(); // 播放3s后调用暂停接口暂停播放。
}, 3000);
}
this.count++;
break;
case 'completed': // 播放结束后触发该状态机上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state completed called.`);
let eventDataFalse: emitter.EventData = {
data: {
'flag': false
}
};
let innerEvent: emitter.InnerEvent = {
eventId: 1,
priority: emitter.EventPriority.HIGH
};
emitter.emit(innerEvent, eventDataFalse);
if (this.intervalID != -1) {
clearInterval(this.intervalID)
}
this.avPlayer.off('bufferingUpdate')
AppStorage.setOrCreate('currentTime', this.durationTime);
break;
case 'released':
console.info(`${this.tag}: setAVPlayerCallback released called.`);
break
case 'stopped':
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state stopped called.`);
break
case 'error':
console.error(`${this.tag}: setAVPlayerCallback AVPlayer state error called.`);
break
case 'paused':
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state paused called.`);
setTimeout(() => {
console.info('AVPlayer paused wait to play again');
this.avPlayer?.play(); // 暂停3s后再次调用播放接口开始播放。
}, 3000);
break
default:
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state unknown called.`);
break;
}
});
// 时间上报监听函数
this.avPlayer.on('timeUpdate', (time: number) => {
this.currentTime = time;
});
}
aboutToAppear() {
this.windowWidth = display.getDefaultDisplaySync().width;
this.windowHeight = display.getDefaultDisplaySync().height;
this.surfaceW = this.windowWidth * SURFACE_W;
this.surfaceH = this.surfaceW / SURFACE_H;
this.isPaused = true;
this.context = getContext(this) as common.UIAbilityContext;
}
aboutToDisappear() {
if (this.avPlayer == null) {
console.info(`${this.tag}: avPlayer has not init aboutToDisappear`);
return;
}
this.avPlayer.release((err) => {
if (err == null) {
console.info(`${this.tag}: videoRelease release success`);
} else {
console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
}
});
emitter.off(innerEventFalse.eventId);
}
onPageHide() {
this.avPause();
this.isPaused = false;
}
onPageShow() {
//const params = router.getParams();
//this.fileName = params.toString();
console.log("myData2",this.myData);
this.fileName = this.myData+".mp3";
console.log("myData2",this.fileName);
emitter.on(innerEventTrue, (res: emitter.EventData) => {
if (res.data) {
this.isPaused = res.data.flag;
this.XComponentFlag = res.data.flag;
}
});
emitter.on(innerEventFalse, (res: emitter.EventData) => {
if (res.data) {
this.isPaused = res.data.flag;
}
});
emitter.on(innerEventWH, (res: emitter.EventData) => {
if (res.data) {
this.windowWidth = res.data.width;
this.windowHeight = res.data.height;
this.setVideoWH();
}
});
}
setVideoWH(): void {
if (this.percent >= 1) { // 横向视频
this.surfaceW = Math.round(this.windowWidth * PROPORTION);
this.surfaceH = Math.round(this.surfaceW / this.percent);
} else { // 纵向视频
this.surfaceH = Math.round(this.windowHeight * PROPORTION);
this.surfaceW = Math.round(this.surfaceH * this.percent);
}
}
@Builder
CoverXComponent() {
XComponent({
// 装载视频容器
id: 'xComponent',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.id('VideoView')
.visibility(this.XComponentFlag ? Visibility.Visible : Visibility.Hidden)
.onLoad(() => {
this.surfaceId = this.xComponentController.getXComponentSurfaceId();
this.avSetupAudio();
})
.height(`${this.surfaceH}px`)
.width(`${this.surfaceW}px`)
}
build() {
Column() {
Stack() {
Column() {
this.CoverXComponent()
}
.align(Alignment.TopStart)
.margin({ top: 80 })
.id('VideoView')
.justifyContent(FlexAlign.Center)
Row(){
Image($r('app.media.vinyl'))
.width('100%')
.height('40%')
}
Text()
.height(`${this.surfaceH}px`)
.width(`${this.surfaceW}px`)
.margin({ top: 80 })
.backgroundColor(Color.Black)
.opacity($r('app.float.size_zero_five'))
.visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)
Row() {
Text(this.timeConvert(this.currentTime))
.id("currentTime")
.fontSize($r('app.float.size_24'))
.opacity($r('app.float.size_1'))
.fontColor($r("app.color.slider_selected"))
Text("/" + this.timeConvert(this.durationTime))
.id("durationTime")
.fontSize($r('app.float.size_24'))
.opacity($r('app.float.size_1'))
.fontColor(Color.White)
}
.margin({ top: 80 })
.visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)
Column() {
Blank()
Column() {
// 进度条
Row() {
Row() {
// 播放、暂停键
Image(this.isPaused ? $r("app.media.ic_video_play") : $r("app.media.ic_video_pause"))// 暂停/播放
.id(this.isPaused ? 'pause' : 'play')
.width($r('app.float.size_40'))
.height($r('app.float.size_40'))
.onClick(() => {
if (this.isPaused) {
this.avPause();
this.isPaused = false;
} else {
this.avPlay();
this.isPaused = true;
}
})
// 左侧时间
Text(this.timeConvert(this.currentTime))
.id("currentTimeText")
.fontColor(Color.White)
.textAlign(TextAlign.End)
.fontWeight(FontWeight.Regular)
.margin({ left: $r('app.float.size_10') })
}
// 进度条
Row() {
Slider({
value: this.currentTime,
min: 0,
max: this.durationTime,
style: SliderStyle.OutSet
})
.id('Slider')
.blockColor(Color.White)
.trackColor(Color.Gray)
.selectedColor($r("app.color.slider_selected"))
.showTips(false)
.onChange((value: number, mode: SliderChangeMode) => {
if (this.seekTime !== value) {
this.seekTime = value;
this.avSeek(Number.parseInt(value.toFixed(0)), mode);
}
})
}
.layoutWeight(1)
Row() {
// 右侧时间
Text(this.timeConvert(this.durationTime))
.id("durationTimeText")
.fontColor(Color.White)
.fontWeight(FontWeight.Regular)
// 倍速按钮
Button(this.speedName, { type: ButtonType.Normal })
.border({ width: $r('app.float.size_1'), color: Color.White })
.width(75)
.height($r('app.float.size_40'))
.fontSize($r('app.float.size_15'))
.borderRadius($r('app.float.size_24'))
.fontColor(Color.White)
.backgroundColor(Color.Black)
.opacity($r('app.float.size_1'))
.margin({ left: $r('app.float.size_10') })
.id('Speed')
.onClick(() => {
this.speedIndex = (this.speedIndex + 1) % SPEED_COUNT;
this.speedSelect = this.speedIndex;
this.speedName = this.speedList[this.speedIndex];
if(!this.avPlayer) return;
switch (this.speedSelect) {
case 0:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
break;
case 1:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
break;
case 2:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
break;
case 3:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
break;
}
})
}
}
.justifyContent(FlexAlign.Center)
.padding({ left: $r('app.float.size_25'), right: $r('app.float.size_30') })
.width('100%')
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
}
.backgroundColor(Color.Black)
.height('90%')
.width('100%')
Row() {
Text(this.fileName)
.fontSize($r('app.float.size_20'))
.fontColor(Color.White)
.opacity($r('app.float.size_zero_six'))
.fontWeight(FontWeight.Regular)
.textAlign(TextAlign.Center)
}
Row() {
Text("")
.fontSize($r('app.float.size_20'))
.fontColor(Color.White)
.opacity($r('app.float.size_zero_six'))
.fontWeight(FontWeight.Regular)
.textAlign(TextAlign.Center)
}
}.backgroundColor(Color.Black)
.height('100%')
.width('100%')
}
}
2.3 App.ets文件,通过LocalStorage实现组件间数据共享
// App.ets
const storage = new LocalStorage();
export default storage;
三、显示效果