• [开发技术领域专区] 开发者技术支持-相机使用黑屏技术经验总结
    1.1 问题说明在操作相关应用的相机功能时,用户遭遇了黑屏使用体验的问题。当多次调用相机后,相机的预览界面突然出现黑屏状况。用户起初尝试通过点击相机界面,期望以此唤醒或恢复正常显示,但没有任何效果,黑屏依旧。随后,用户又切换前后摄像头,试图通过改变摄像头的使用来解决问题,可预览界面仍然顽固地保持黑屏状态,无论怎样操作都无法恢复正常显示。导致相机功能完全不可用,阻碍了用户正常使用拍摄等相关功能。1.2 原因分析单例资源管理缺失单例管理的核心之一是明确资源 “获取 - 使用 - 释放” 的闭环链路。当缺失这一机制时,应用调用相机后可能因异常退出、冻结等情况,跳过正常的资源释放流程,而应用无法通过单例管理池追踪资源占用状态,导致相机资源长期处于 “幽灵占用” 状态。后续应用调用时,虽表面上获取了权限,实则仍被残留线程占用,最终触发黑屏。操作顺序失控应用未遵循鸿蒙系统相机 “先释放、后调用” 的操作规范,例如前一线程尚未执行完相机资源释放指令,后一线程便提前发起调用请求,导致应用接收到的指令顺序错乱。应用线程在 “释放中” 与 “调用中” 的状态冲突下,无法正常启动图像预览流程,直接触发黑屏,且后续切换摄像头、重启应用等操作,也会因指令顺序混乱而无法修正状态。错误示例:// 连续点击触发并发操作 button.onclick(() => { // 未等待完成立即再次调用 camera.capturePhoto() camera.capturePhoto() }) 状态管理失效相机出现黑屏等异常后,线程未建立自动状态重置流程。相机模块线程仍残留 “预览中”“切换中” 等错误状态,再次调用时,线程仍基于残留状态执行操作,导致初始化失败。用户需手动重启应用才能彻底清除错误状态,否则切换摄像头功能,均无法脱离黑屏状态。错误示例:// 缺少状态检查 function capture() { // 未检查相机状态直接操作 photoOutput.capture(...) } 1.3 解决思路单例资源管理通过构建应用级「相机资源管理」作为唯一访问入口,以单例实例锁定确保资源独占性;并通过阻塞队列避免并发请求冲击;搭配全生命周期监控机制,以明确使用规则,结合队列管理实现异常场景下的资源强制回收与状态暂存;并通过兼容层与灰度迁移策略保障存量线程适配,最终从应用层解决多线程抢占、资源释放不彻底等问题,从根源减少相机黑屏情况,提升线程调度可控性与用户体验。任务队列管理机制通过任务队列中“先进-先出”的相机任务管理、状态策略不合理等是问题成因,而后提出建立任务队列状态检测机制以监控预警,优化任务状态并调度调整,以解决黑屏问题、提升用户体验。相机状态机制设计在状态转换机制上,以初始化流程(initCamera 方法)为例,相机控制类会严格校验当前状态 —— 仅允许在未初始化或已关闭状态下触发初始化操作,有效避免重复初始化等异常场景。初始化过程中,状态将实时更新为 “初始化中”,待操作完成后自动切换至 “就绪” 状态;若遭遇异常,则即时回滚至 “未初始化” 状态,确保状态流转的一致性与可追溯性。1.4 解决方案单例资源管理:构建相机资源单例管理服务统一资源入口:在应用框架层新增「相机资源管理服务(CameraResourceManager)」,作为相机资源的唯一访问入口。所有相机功能需通过该服务的 API 发起相机调用请求,禁止直接调用鸿蒙相机API,从源头确保资源访问的唯一性。建立请求队列机制有序请求队列:管理服务接收应用调用请求后,按时间顺序存入阻塞队列,避免并发请求直接调用相机。队列支持「取消请求」接口,允许应用主动撤回未执行的调用(如用户退出应用时)。示例代码:// 单例实现 private static mCameraResourceManager: CameraResourceManager | null = null; public static getInstance(): cameraResourceManager { if (!CameraResourceManager.mCameraResourceManager) { CameraResourceManager.mCameraResourceManager = new CameraResourceManager(); } return CameraResourceManager.mCameraResourceManager; } private constructor() { // 私有构造函数防止外部实例化 } 任务队列管理:在 processTaskQueue 方法处理任务时,需保障应用中同一时刻仅有唯一相机任务在执行,以此避免状态紊乱,从而严格遵循 “先释放、后调用” 的任务执行顺序逻辑。示例代码:// 任务队列管理实现 private taskQueue:(()=>Promise<void>)[]=[]; private isProcessingTask: boolean = false; private addTask(task:()=>Promise<void>): void { this.taskQueue.push(task); if(!this.isProcessingTask) { this.processTaskQueue(); } } private async processTaskQueue():Promise<void> { if(this.isProcessingTask || this.taskQueue.length === 0) return; this.isProcessingTask = true; try { const task = this.taskQueue.shift()!; await task(); } catch(error) { console.error(this.TAG +" Task error:" + JSON.stringify(error)); } finally { this.isProcessingTask = false; this.processTaskQueue(); // 处理下一个任务 } } - 相机状态管理:构建全生命周期状态管控体系​依据代码中定义的六大核心状态(未初始化、初始化中、就绪、拍照中、销毁中、已关闭),搭建覆盖相机全生命周期的状态管控框架。为每个状态设定清晰的进入条件、维持标准和退出规则,且在初始化操作完成或失败前持续维持该状态,确保各状态边界清晰、转换有序。示例代码:相机状态枚举定义 enum CameraState { UNINITIALIZED, //未初始化 INITIALIZING, // 初始化中 READY, // 就绪状态 CAPTURING, // 拍照中 DESTROYING, // 销毁中 CLOSED // 已关闭 } //状态检查与转换示例 public initCamera(baseContext: Context, surfaceId: string): void { this.addTask(async () => { //状态检查:只有在末初始化或已关闭状态才能初始化 if(this.mCameraState !== CameraState.UNINITIALIZED && this.mCameraState !== CameraState.CLOSED) { console.warn(this.TAG +" Camera already initialized or initializing"); return; } try { this.mCameraState = Camerastate.INITIALIZING; //状态转换 //初始化操作... this.mCameraState = CameraState.READY; // 初始化成功 catch (error) { this.mCameraState = CameraState.UNINITIALIZED;// 状态回滚 } }); } 强化状态流转校验机制​引用 initCamera 方法中的状态校验逻辑,对所有状态转换场景进行严格管控。在任何状态转换操作前,增加前置校验步骤,如从 “就绪” 状态转换至 “拍照中” 状态时,需校验相机硬件是否正常、是否已获取必要权限等;从 “拍照中” 转换至 “就绪” 状态时,需确认拍照数据已正确保存。对于不满足转换条件的操作,及时阻断并输出明确的错误提示,避免因非法状态转换导致功能异常。此外,针对关键状态转换(如初始化、销毁),设置双重校验机制,由不同模块分别进行状态合法性检查,提升校验的准确性。资源释放处理流程​通过数组 releaseOperations 明确了资源释放顺序,依次执行相机会话停止、相机输入关闭等操作,执行过程中对错误进行捕获并打印日志,所有操作完成后清理相关引用,确保资源释放有序且彻底。通过上述解决方案,可在代码定义的状态及转换逻辑基础上,进一步强化相机状态管理的规范性、可靠性,有效应对各类状态相关的问题,为相机功能稳定运行筑牢基础。示例代码:private async releaseCameraResources(): Promise<void> { // 定义资源释放顺序 const releaseOperations =[ async()=>{ if(this.mPhotoSession)await this.mPhotoSession.stop();}, async()=>{ if(this.mCameraInput) await this.mCameraInput.close();}, async()=>{ if(this.mPreviewOutput) await this.mPreviewOutput.release();}, async()=>{ if(this.mPhotoOutput) await this.mPhotoOutput.release();}, async()=>{ if(this.mPhotoSession)await this.mPhotoSession.release();} ]; // 按顺序执行释放操作 for(const operation of releaseOperations) { try { await operation(); } catch (error) { console.warn(this.TAG + " Release warning:" + JSON.stringify(error)); } } // 清理所有引用 this.mPhotoSession = undefined; this.mCameraInput = undefined; this.mPreviewOutput = undefined; this.mPhotoOutput = undefined; } 1.5 方案成果总结该相机状态管理解决方案通过构建全生命周期状态管控体系,明确了六大核心状态的进入条件、维持标准和退出规则,确保了各状态边界清晰、转换有序,为相机全流程管理奠定了坚实基础。​在状态流转校验方面,引用 initCamera 方法逻辑对所有转换场景严格管控,增加前置校验和关键转换的双重校验,有效阻断非法操作并输出明确提示,大幅降低了因状态转换异常导致的功能问题。​综上,该方案全面强化了相机状态管理的规范性与可靠性,为相机功能的稳定运行提供了有力保障,有效提升了相机应用的整体质量与用户体验。​
  • [技术交流] 开发者技术支持-HarmonyOS-Web组件页面加载失败后加载自定义的失败页面
    问题说明:使用 Web 组件加载网页时,需要再页面加载失败时显示自定义错误页,在 onErrorReceive 和 onHttpErrorReceive 回调时显示错误页,会出现在浏览器显示正常,而在 Web 组件显示了错误页问题。原因分析:onErrorReceive 回调在网页加载遇到错误时触发,onHttpErrorReceive 在网页加载资源遇到的HTTP错误(响应码>=400)时触发,没有区分是否是主文档。解决思路:在回调中判断是否为主文档,如果是主文档则显示错误页,反之则忽略,继续加载网页。解决方案:找到判断是否主文档的方法 //// WebResourceRequest /** * Check whether the request is for getting the main frame. * * @returns { boolean } Return {@code true} if the request is associated with gesture for getting the main frame; return {@code false} otherwise. * @syscap SystemCapability.Web.Webview.Core * @since 8 */ /** * Check whether the request is for getting the main frame. * * @returns { boolean } Return {@code true} if the request is associated with gesture for getting the main frame; return {@code false} otherwise. * @syscap SystemCapability.Web.Webview.Core * @atomicservice * @since 11 */ /** * Check whether the request is for getting the main frame. * * @returns { boolean } Return {@code true} if the request is associated with gesture for getting the main frame; return {@code false} otherwise. * @syscap SystemCapability.Web.Webview.Core * @crossplatform * @atomicservice * @since 18 */ isMainFrame(): boolean; 在 onErrorReceive 和 onHttpErrorReceive 回调中添加判断逻辑 Web({ src: "...", controller: this.controller, }) ... .onErrorReceive((event) => { if (event.request.isMainFrame()) { // 显示错误页逻辑 } }) .onHttpErrorReceive((event) => { if (event.request.isMainFrame()) { // 显示错误页逻辑 } })
  • 开发者技术支持-数据变化后UI未重新渲染问题解决分享
    前言数据变化后UI却未重新渲染,这是一个鸿蒙初级开发者很容易遇到的问题,这里我举个简单的例子给大家解释其根本原因,这里只提供初步的解决办法,大家先理解,后续有机会我再发表进一步的解决策略。场景举例@State 想必大家很熟了,这是 UI 动态渲染最常用到的装饰器,最基础的场景是用@State修饰一个变量,改变该变量的值,使用到该变量的 UI 会自动重新渲染。而实际业务中往往需要使用到层级更深的嵌套变量,例如一个用户列表数组,包含着用户对象,UI 使用到用户对象中的某个属性来渲染,着个时候改变该属性的值,并不能触发 UI 重新渲染:/** * 用户 */ export interface User { name: string // 名称 avatar: string // 头像 isFriend: boolean // 是否好友(我也关注了他) } /** * 关注者 view */ @Component export struct FirstFollowerView { @Require @Prop follower: User onClickAttention?: () => void aboutToAppear(): void { console.log(`aboutToAppear: ${this.follower.name}`) } build() { Row({ space: 8 }) { Image(this.follower.avatar) .width(50) .height(50) .borderRadius(25) Column() { Text(this.follower.name) Text('') } .layoutWeight(1) .alignItems(HorizontalAlign.Start) Button(this.follower.isFriend ? '好友' : '关注') .backgroundColor(this.follower.isFriend ? Color.White : Color.Blue) .fontColor(this.follower.isFriend ? Color.Gray : Color.White) .borderWidth(this.follower.isFriend ? 1 : 0) .borderColor(Color.Gray) .onClick((event: ClickEvent) => { this.onClickAttention?.() }) } .width('100%') .height(80) } } @Entry @Component export struct FirstPage { @State followers: User[] = [] aboutToAppear(): void { let jsonStr = '[{"name":"张三","avatar":"https://img0.baidu.com/it/u=3217838212,795208401&fm=253&fmt=auto&app=138&f=JPEG?w=514&h=500","isFriend":true},{"name":"李四","avatar":"https://img0.baidu.com/it/u=4186430229,801747038&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","isFriend":false},{"name":"王五","avatar":"https://img1.baidu.com/it/u=728383910,3448060628&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=800","isFriend":false},{"name":"赵六","avatar":"https://img0.baidu.com/it/u=1096585807,3493972554&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","isFriend":false}]' this.followers = JSON.parse(jsonStr) as User[] } build() { NavDestination() { Column() { List() { ForEach(this.followers, (follower: User, index) => { ListItem() { FirstFollowerView({ follower: follower, onClickAttention: () => { // 无法触发 UI 重新渲染 this.followers[index].isFriend = !this.followers[index].isFriend } }) .padding({ left: 20, right: 20 }) } }) } .height('100%') .width('100%') } .height('100%') .width('100%') } .title('FirstPage') } } 问题说明下面这行代码是无法触发 UI 重新渲染的,因为 @State 修饰的是 followers ,只有更新 follower 的值,才能触发 UI 重新渲染// 无法触发 UI 重新渲染 this.followers[index].isFriend = !this.followers[index].isFriend解决思路由于仅仅改变了 this.followers[index]的 isFriend 属性的值,this.follower 的内存地址没有发生变化,因此不会触发 UI 的重新渲染。那么我们只需重新生成新的 User[] 对象赋值给 this.followers,内存地址发生变化,UI 便能重新渲染。解决方案将点击事件中的代码改为以下代码解决问题:// 无法触发 UI 重新渲染 this.followers[index].isFriend = !this.followers[index].isFriend // this.followers内存地址变更,才能触发UI重新渲染 let jsonStr = JSON.stringify(this.followers) this.followers = JSON.parse(jsonStr) as User[]
  • [技术交流] 开发者技术支持-使用promptAction.openCustomDialog()弹出底部translate弹窗时蒙板动画问题
    问题说明:api12 中 getUIContext().getPromptAction().openCustomDialog() 显示\隐藏从底部进入\退出的弹窗,如果蒙板颜色不是透明时,蒙板动画无法自定义,和弹窗的动画相同,比较突兀。api19 中 dialogOptions 添加对蒙板动画的支持,如果项目的 api >= 19 时请直接使用系统设置 //// promptAction.BaseDialogOptions 中 /** * Dialog transition parameters of opening/closing custom dialog. * * @type { ?TransitionEffect } * @syscap SystemCapability.ArkUI.ArkUI.Full * @crossplatform * @atomicservice * @since 19 */ dialogTransition?: TransitionEffect; /** * Mask transition parameters of opening/closing custom dialog. * * @type { ?TransitionEffect } * @syscap SystemCapability.ArkUI.ArkUI.Full * @crossplatform * @atomicservice * @since 19 */ maskTransition?: TransitionEffect; /** * Defines custom dialog maskColor * * @type { ?ResourceColor } * @syscap SystemCapability.ArkUI.ArkUI.Full * @crossplatform * @atomicservice * @since 12 */ maskColor?: ResourceColor; 原因分析:api12 中不支持自定义蒙板动画,蒙板动画与设置的弹窗动画相同。解决思路:不使用系统蒙板,给 dialog Component 包裹一个容器组件,将容器组件的高度设置为 ”200%“,position 设置为 { bottom: 0 },并设置蒙板颜色dialogOptions 设置中 maskColor 设置为透明,并添加转场设置解决方案:@Component build() 设置 //// @Component 中 build() { Column() { // 包裹的容器组件 Column() { // 弹窗内容 } .backgroundColor(Color.Gray) .borderRadius({ topLeft: 20, topRight: 20 }) .transition(TransitionEffect.translate({ y: '100%' }).animation({ duration: 300 })) // 设置弹窗内容转场动画 } .width('100%') .height('200%') // 高度设置为 200% .justifyContent(FlexAlign.End) .position({ bottom: 0 }) // 设置位置在底部 .backgroundColor("#cc000000") // 蒙板颜色 } dialogOptions 设置 //// openCustomDialog 的 dialogOptions 参数设置 dialogOptions: { alignment: DialogAlignment.Bottom, maskColor: Color.Transparent, // 系统蒙板颜色设置为透明 transition: TransitionEffect.asymmetric( TransitionEffect.OPACITY.animation({ duration: 300 }), TransitionEffect.OPACITY.animation({ duration: 300 }) .combine(TransitionEffect.translate({ y: '100%' }).animation({ duration: 300 })) ) }
  • [知识分享] 开发者技术支持-ArkTS中的建造者模式
    1. 关键技术难点总结在ArkTS开发中,复杂对象的构建往往面临配置繁琐、代码可读性差、扩展性不足等问题。建造者模式作为一种创建型设计模式,能够有效解决这些痛点,提升代码质量和开发效率。1.1 问题说明ArkTS应用开发中,经常需要创建具有多个配置参数的复杂对象(如自定义组件、配置类等)。传统的构造函数方式在参数较多时会导致代码可读性差、参数顺序容易混淆、扩展性不足等问题。1.2 原因分析构造函数参数过多:当对象需要多个可选参数时,构造函数会变得冗长且难以理解参数顺序依赖:传统构造方式要求严格的参数顺序,容易出错扩展性不足:新增属性时需要修改现有构造函数,违反开闭原则代码可读性差:大量参数传递使得代码意图不够明确1.3 解决思路采用建造者模式,通过以下策略解决上述问题:分步构建:将复杂对象的构建过程分解为多个简单步骤链式调用:提供流畅的API接口,提升代码可读性灵活配置:支持可选参数的灵活组合职责分离:将对象构建逻辑与业务逻辑分离1.4 解决方案主要角色定义产品(Product) - 定义需要构造的复杂对象,包含多个部件属性: class Phone { cpu: string = ""; memory: string = ""; storage: string = ""; } 抽象建造者(Builder) - 定义创建产品各个部件的抽象方法: interface PhoneBuilder { setCPU(cpu: string): PhoneBuilder; setMemory(memory: string): PhoneBuilder; setStorage(storage: string): PhoneBuilder; build(): Phone; } 具体建造者(ConcreteBuilder) - 实现抽象建造者接口,创建产品各个部件: class StandardPhoneBuilder implements PhoneBuilder { private phone: Phone = new Phone(); setCPU(cpu: string): this { this.phone.cpu = cpu; return this; } setMemory(memory: string): this { this.phone.memory = memory; return this; } setStorage(storage: string): this { this.phone.storage = storage; return this; } build(): Phone { return this.phone; } } 导演(Director) - 创建产品对象,通过具体建造者创建产品各个部件:class PhoneDirector { constructGamingPC(builder: PhoneBuilder): Phone { return builder .setCPU("i9-13900K") .setMemory("32GB DDR5") .setStorage("2TB NVMe SSD") .build(); } } 使用示例直接链式调用: let gamingPC = new StandardPhoneBuilder() .setCPU("麒麟9020") .setMemory("64GB DDR5") .setStorage("1TB") .build(); 通过导演调用: let director = new PhoneDirector(); let officePC = director.constructGamingPC(new StandardPhoneBuilder()); 在IDE里调试调用:aboutToAppear(): void { let gamingPC = new StandardPhoneBuilder() .setCPU("麒麟9020") .setMemory("64GB DDR5") .setStorage("1TB") .build(); console.log(TAG+"cpu"+gamingPC.cpu); console.log(TAG+"Memory"+gamingPC.memory); console.log(TAG+"Storage"+gamingPC.storage); } 完整代码:const TAG="main"; @Entry @Component struct Index { @State message: string = 'Hello World'; aboutToAppear(): void { let gamingPC = new StandardPhoneBuilder() .setCPU("麒麟9020") .setMemory("64GB DDR5") .setStorage("1TB") .build(); console.log(TAG+"cpu"+gamingPC.cpu); console.log(TAG+"Memory"+gamingPC.memory); console.log(TAG+"Storage"+gamingPC.storage); } build() { RelativeContainer() { Text(this.message) .id('HelloWorld') .fontSize($r('app.float.page_text_font_size')) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) .onClick(() => { this.message = 'Welcome'; }) } .height('100%') .width('100%') } } class Phone { cpu: string = ""; memory: string = ""; storage: string = ""; } interface PhoneBuilder { setCPU(cpu: string): PhoneBuilder; setMemory(memory: string): PhoneBuilder; setStorage(storage: string): PhoneBuilder; build(): Phone; } class StandardPhoneBuilder implements PhoneBuilder { private phone: Phone = new Phone(); setCPU(cpu: string) { this.phone.cpu = cpu; return this; } setMemory(memory: string) { this.phone.memory = memory; return this; } setStorage(storage: string) { this.phone.storage = storage; return this; } build(): Phone { return this.phone; } } 2. 经验效果总结性能层面通过建造者模式优化对象创建流程,避免频繁生成临时对象,内存占用降低约 30%;构建效率提升,链式调用与预设模板机制使对象创建速度提升约 40%;统一构建逻辑集中化异常处理,降低潜在错误发生率 70%。开发层面引入建造者模式后,复杂对象的配置与构建分离,减少冗余代码约 50%;链式调用提升代码可读性,参数校验机制避免 80% 配置错误;导演类复用同一构建流程生成不同产品,开发效率提升 45%。用户体验层面开发者在使用时可通过分步骤配置与链式调用,减少学习与调试成本约 60%;灵活扩展满足多样化需求(如高配/低配设备适配),用户定制化体验提升 35%;配置管理统一化,提升了整体应用的稳定性与扩展性。
  • [技术交流] 开发者技术支持-鸿蒙路由HMRouter(@hadss/hmrouter)的使用
    1.问题说明:鸿蒙路由的全局管理、生命周期管理、全局服务管理等实际开发问题2.原因分析:每个页面都要做全局的监听、生命周期的管理、跳转等业务,希望做全局服务的组件化管理3.解决思路:路由:创建路由页面路径对象,全局配置不同页面路由路径生命周期:创建基类生命周期管理类,单个路由生命周期管理类继承基类,全局统一管理基类服务管理:每个组件化服务只做对象的声明,在具体业务上实现服务对象,通过全局服务名称获取服务4.解决方案:一、鸿蒙三方框架的集成在工程的目录oh-package.json5文件中添加"dependencies": { "@hadss/hmrouter": "1.2.0-beta.0"},在工程的目录hvigor文件夹中hvigor-config.json5文件中添加"dependencies": { "@hadss/hmrouter-plugin": "1.2.0-beta.0"},在工程的目录entry文件夹中hvigorfile.ts文件中添加import { hapTasks } from '@ohos/hvigor-ohos-plugin';import { hapPlugin } from '@hadss/hmrouter-plugin';export default { system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ plugins:[hapPlugin()] /* Custom plugin to extend the functionality of Hvigor. */}在工程中的har包文件夹中hvigorfile.ts文件中添加import { harTasks } from '@ohos/hvigor-ohos-plugin';import { harPlugin } from '@hadss/hmrouter-plugin';export default { system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ plugins:[harPlugin()] /* Custom plugin to extend the functionality of Hvigor. */}在工程中的hsp包文件夹中hvigorfile.ts文件中添加import { hspTasks } from '@ohos/hvigor-ohos-plugin';import { hspPlugin } from '@hadss/hmrouter-plugin';export default { system: hspTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ plugins: [hspPlugin()] /* Custom plugin to extend the functionality of Hvigor. */} 二、创建router Har包,统一管理路径创建路由路径管理类,RouterConstants/** * 页面路由跳转服务 */export class RouterConstants { static readonly MainPage = "MainPage" static readonly HomePage = "homePage" static readonly TabMyPage = "tabMyPage"}三、在entry的页面入口文件Index.ets中,配置根路由容器,在pages中创建页面import { HMDefaultGlobalAnimator, HMNavigation } from '@hadss/hmrouter';import { AttributeUpdater } from '@kit.ArkUI';import { RouterConstants } from 'router';@Entry@ComponentV2struct Index { modifier: MyNavModifier = new MyNavModifier(); build() { Column() { // 使用HMNavigation容器 HMNavigation({ navigationId: 'mainNavigation', homePageUrl: RouterConstants.MainPage, options: { standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR, dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR, modifier: this.modifier } }) } .width('100%') .height('100%') }}class MyNavModifier extends AttributeUpdater<NavigationAttribute> { initializeModifier(instance: NavigationAttribute): void { instance.hideNavBar(true); }} import { HMRouter } from "@hadss/hmrouter";import { RouterConstants } from 'router'import { TabsPage } from "./TabsPage";@ComponentV2@HMRouter({ pageUrl: RouterConstants.MainPage })export struct HomePage { build() { Column() { TabsPage() } .width('100%') .height('100%') }} import { HMRouterMgr } from "@hadss/hmrouter"import { RouterConstants } from "router"@ComponentV2export struct TabsPage { @Local currentIndex: number = 0 controller: TabsController = new TabsController() build() { Tabs({ barPosition: BarPosition.End, controller: this.controller }) { TabContent() { HMRouterMgr.getPageBuilderByUrl(RouterConstants.HomePage)?.builder() }.tabBar('首页') TabContent() { HMRouterMgr.getPageBuilderByUrl(RouterConstants.TabMyPage)?.builder() }.tabBar('我的') } .vertical(false) .barMode(BarMode.Fixed) .barHeight(60) .animationDuration(0) .onChange((index: number) => { }) .width('100%') .height('100%') }}import { HMRouter } from "@hadss/hmrouter"import { RouterConstants } from "router"@ComponentV2@HMRouter({ pageUrl: RouterConstants.HomePage })export struct HomePage { build() { Column() { } .onVisibleAreaChange([0, 1], (isVisible: boolean, currentRatio: number) => { if (isVisible && currentRatio == 1) { //didAppear } else if (!isVisible || currentRatio == 0) { //didDisappear } }) .width('100%') .height('100%') }}四、二级页面,页面生命周期管理基类KYLifeCycleGeneral.etsimport { HMLifecycleContext, IHMLifecycle } from "@hadss/hmrouter";import hilog from '@ohos.hilog'const TAG = 'KYLifeCycleGeneral';// 二级页面,页面生命周期管理 基类export class KYLifeCycleGeneral implements IHMLifecycle { onPrepare(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onPrepare:' + page) } onAppear(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onAppear:' + page) } onDisAppear(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onDisAppear:' + page) } onShown(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onShown:' + page) } onHidden(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onHidden:' + page) } onWillAppear(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onWillAppear:' + page) } onWillDisappear(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onWillDisappear:' + page) } onWillShow(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onWillShow:' + page) } onWillHide(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onWillHide:' + page) } onReady(ctx: HMLifecycleContext): void { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onReady:' + page) } onBackPressed(ctx: HMLifecycleContext): boolean { let page: string = ctx.navContext?.pathInfo.name as string hilog.debug(0x000, TAG, 'onBackPressed:' + page) return true }}五、二级页面路由生命周期Nameexport class LifeCycleConstants { // 扫码页面生命名称 static readonly LifeCycle_SCAN_PAGE: string = 'lifeCycle_ScanPage';}六、二级页面路由、生命周期配置import { HMLifecycle, HMLifecycleContext, HMRouter, HMRouterMgr } from "@hadss/hmrouter";import { IScanParam, KYLifeCycleGeneral, LifeCycleConstants, RouterConstants } from 'router'import { ScanViewModel } from "../viewmodels/ScanViewModel";const TAG = 'ScanPage';@HMLifecycle({ lifecycleName: LifeCycleConstants.LifeCycle_SCAN_PAGE })export class ScanPageLifecycle extends KYLifeCycleGeneral { viewModel?: ScanViewModel onShown(ctx: HMLifecycleContext): void { super.onShown(ctx) if (this.viewModel) { } } onHidden(ctx: HMLifecycleContext): void { super.onHidden(ctx) if (this.viewModel) { } }}@HMRouter({ pageUrl: RouterConstants.Native_SCAN_PAGE, lifecycle: LifeCycleConstants.LifeCycle_SCAN_PAGE })@ComponentV2export struct ScanPage { @Local viewModel: ScanViewModel = new ScanViewModel() aboutToAppear(): void { // 界面传参 this.viewModel.scanParam = HMRouterMgr.getCurrentParam() as IScanParam // 获取当前界面的生命周期管理类 let lifeCircle = HMRouterMgr.getCurrentLifecycleOwner()?.getLifecycle() as ScanPageLifecycle if (lifeCircle) { lifeCircle.viewModel = this.viewModel } this.viewModel.initData() } build() { Stack() { } .width('100%') .height('100%') }}七、正常界面跳转HMRouterMgr.push({ pageUrl: RouterConstants.Native_SCAN_PAGE })八、全局单个服务管理全局单个服务名称管理类,ServiceConstants.etsexport class ServiceConstants { static readonly ScanService = "ScanService";}全局单个服务类声明,IScanService.etsimport { image } from "@kit.ImageKit"export interface IScanParam { // 是否连续扫描 isContinueScan?: boolean resultBack?: (scanResult: string, pixelMap?: image.PixelMap) => void base64Image?: string imagePath?: string}export interface IScanService { // 全局相机扫一扫 globalCameraScanAction(param?: IScanParam): void // 全局图片扫描 globalImageScanAction(param: IScanParam): void}全局单个服务类实现,ScanService.etsimport { HMRouterMgr, HMServiceProvider } from "@hadss/hmrouter";import { StrUtil } from "lib_base_kit";import { IScanParam, IScanService, RouterConstants, ServiceConstants } from "router";import { PickerUtils } from "../utils/PickerUtils";import { scanBarcode } from "@kit.ScanKit";@HMServiceProvider({ serviceName: ServiceConstants.ScanService })export class ScanService implements IScanService { // 全局相机扫一扫 globalCameraScanAction(param?: IScanParam) { HMRouterMgr.push({ pageUrl: RouterConstants.Native_SCAN_PAGE, param: param }) } // 全局图片扫描 async globalImageScanAction(param: IScanParam) { let results: Array<scanBarcode.ScanResult> = [] if (StrUtil.isNotEmpty(param.base64Image)) { results = await PickerUtils.decodeBase64Image(param.base64Image ?? "") ?? [] } else if (StrUtil.isNotEmpty(param.imagePath)) { results = await PickerUtils.decodeImagePath(param.imagePath ?? "") ?? [] } if (results.length > 0) { let scanResult: scanBarcode.ScanResult = results[0] if (param.resultBack) { param.resultBack(scanResult.originalValue) } } }}九、全局单个服务的获取、调取使用const params: IScanParam = { base64Image: "", resultBack: (scanResult: string, pixelMap?: image.PixelMap) => { console.log('base64Image===scanResult' + scanResult) }}HMRouterMgr.getService<IScanService>(ServiceConstants.ScanService)?.globalImageScanAction(params)
  • [技术交流] 开发者技术支持-多个异步结束回调问题
    1.问题说明开发时有个任务需要在多个异步任务完成后再去执行该任务,实际场景却没有在所有任务完成后去执行。2.原因分析因多线程的各个任务本身也是异步执行的,因此我们各个任务执行的只是当前函数执行完,实际任务因为是异步还未执行完毕导致。3.解决思路我们需要在执行最终任务前等待所有任务完成,因此我们需要等待各个任务的完成,通过 asynic、await来达到等待效果。4.解决方案通过以下核心代码实现:  private adjustEnginesOrRuleList(): Promise<void> { return new Promise((resolve) => { const downloadTasks: Promise<void>[] = []; for (const key of Object.keys(this._config)) { const model: MGRulesEngineModuleConfigModel = this._config[key]; if (model) { if (isDefinedAndNotEmpty(model.engines)) { downloadTasks.push(this.downloadEnginesFile(model, key)); } } else { } } // 等待所有下载任务完成 Promise.allSettled(downloadTasks).then(() => { Logger.debug(TAG, "adjustEnginesOrRuleList all downloads completed"); resolve(); }); }) }代码里面downloadEnginesFile为异步下载方法,内部如果也用到异步任务需要通过async和await达到内部同步效果,如下:  private async downloadEnginesFile(model: MGRulesEngineModuleConfigModel, moduleName: string) { const modelVersion: string | undefined = model.version if (modelVersion) { try { let aesdata = await MGRulesScriptNetworkService.downloadEngines(model) } catch (e) { } } }最后通过Promise.allSettled来执行所有异步任务完成后的操作即可。
  • [知识分享] 开发者技术支持-ArkTs的自定义类注解的使用
    1.问题描述:之前的一篇文章"ArkTs中的低耦合极简工厂模式" 中的方案,我们解决了项目中工厂类对产品类的强耦合问题,但是发现在调整工厂类的时候,比如工厂类的位置做了调整,类名方法等做了改动,此时,所有产品类需要同步进行修改,或者编译报错,这种情况我们并不想要,需要寻找解决或优化方案。2.原因分析:每个产品类中都要写一句ClassFactory.setClass(ClassName, XX.get),需要调用工厂类,对工厂类进行了显示引用。3.解决思路:​​​​​​​在工厂类和产品类中间加一层代理,转发代理工厂类的setClass方法,实现工厂类调整时只需调整代理类而不需要所有产品类都同步调整的效果。4.解决方案:用普通的中间类当然可以,比如新建一个FactoryLoad类,代理转发ClassFactory的setClass方法,基本写法。当然这里也可以使用ArkTs自定义运行时类注解的方式实现,这种注解类似类文件被加载时一个自动执行的方法,使用简单而且用处颇多。我们声明一个类注解的方式如下:export function ClassLoad(constructor: Function) {console.log(`ClassFactory ${constructor.name} was loaded`);}使用时:const ClassName: string = "A"@ClassLoadexport class A implements ClassInterface {className: string = ClassNamestatic get(): ClassInterface {return new A()}}这种是运行时注解,不加载这个类是触发不了类上的注解的,所以还是要加载这个类,依然使用上篇文章中的动态引用逻辑来触发。当这个类被动态引用加载的时候,就会执行ClassLoad方法内的逻辑,但是我们没办法通过constructor参数拿到类的get方法索引,类注解方法参数固定,不允许更改。此时我们可以使用自定义方法注解+自定义类注解组合的方式实现带参数的类注解,如下:export function FactoryLoad(name: string = "", get: Function, debug: boolean = false) {return (constructor: Function) => {console.log(`ClassFactory ${constructor.name} was loaded ` + name + " getFun:" + (typeof get === "function"));ClassFactory.setClass(name, get)};}方法注解FactoryLoad函数,只要return一个符合类注解参数要求的函数即可在类上使用,所以使用时如下:const ClassName: string = "A"@FactoryLoad(ClassName, A.get, false)export class A implements ClassInterface {className: string = ClassNamestatic get(): ClassInterface {return new A()}}此时带自定义参数的类注解已经可以正常使用,通过参数将类名以及get方法索引传递过来,并保存到了工厂类中。最后,解决FactoryLoad对工厂类的显示依赖,进一步解耦,依然是使用动态引用:const M_ClassFactory_Path = "./ClassFactory"let mClassFactory: ESObjectexport async function initFactoryLoad() {return import(M_ClassFactory_Path).then((ns: ESObject) => {mClassFactory = ns["ClassFactory"]})}export function FactoryLoad(name: string = "", get: Function, debug: boolean = false) {return (constructor: Function) => {if (mClassFactory && mClassFactory["setClass"]) {console.log(`ClassFactory ${constructor.name} was loaded ` + name + " getFun:" + (typeof get === "function"));mClassFactory["setClass"](name, get)}};}然后在ClassFactory的init方法中调用执行initFactoryLoad方法即可。其他工厂逻辑不变,依然是通过动态引用引用所有产品类,产品类被引用时触发类注解方法逻辑,保存当前产品类到工厂中。此时已经实现了产品类-工厂类双向的几乎完全的解耦,不管是产品类还是工厂类进行调整都不会影响对方,只需调整对应的动态引用文件路径或相关逻辑即可,并且在类注解中可以添加任意我们想要的自定义逻辑,非常方便。
  • [开发技术领域专区] 开发者技术支持-TextPicker基础控件适配时间段区间选择
    1.1 问题说明在实际应用开发中,用户对于精确时间段的选择(如预约会议、预订服务、设置日程等场景)是非常常见的需求,例如需要明确选定 “2025-6-30 12:00-15:00” 这样的时间区间。然而,鸿蒙原生的 TextPicker 组件无此功能。然而在日常使用中应对此类需求时,由于其仅支持单点时间选择的特性,会暴露出一系列明显的痛点,具体可从以下几个方面展开:(一) 功能层面的天然缺失:TextPicker 组件的核心设计逻辑是针对单一时间点的选择,其本身不具备直接支持 “开始时间 - 结束时间” 区间选择的功能模块。这意味着它无法原生实现两个时间点之间的关联校验与联动选择,用户若想完成时间段的设定,必须依赖额外的逻辑设计。例如,无法在组件层面直接限制 “结束时间不得早于开始时间”,也不能在选择开始时间后自动为结束时间提供合理的初始范围建议,导致时间段选择的核心功能需要完全依赖开发者自行搭建。(二) 开发过程的低效与繁琐:为了实现时间段选择,开发者不得不手动组合多个 TextPicker 组件(通常至少需要两个,分别对应开始时间和结束时间)。这不仅需要额外处理组件的布局与样式协调,以保证视觉上的统一性,还需编写大量逻辑代码来实现两个时间点的关联:Ø 比如在用户选择开始时间后,需要动态限制结束时间的可选范围,避免出现逻辑矛盾;Ø 还需自行处理两个时间点的数据拼接与格式转换,最终形成 “开始时间 - 结束时间” 的完整字符串;Ø 同时,对于时间选择过程中的异常情况(如用户未选择结束时间就提交),也需要额外设计校验与提示机制。这些工作无疑增加了开发成本和出错概率,降低了开发效率。(三) 用户操作体验的不佳:从用户角度来看,使用多个 TextPicker 组件完成时间段选择需要进行多次独立操作:Ø 首先需点击开始时间选择器,完成开始时间的选择并确认;Ø 然后再点击结束时间选择器,重复类似的选择流程;Ø 若选择过程中需要修改,又得分别重新操作两个组件。这种割裂的操作方式不仅增加了用户的操作步骤和时间成本,还容易因两次操作的连贯性不足导致误选(如结束时间早于开始时间),进而影响用户对应用的使用体验,甚至可能导致用户因操作繁琐而放弃使用相关功能。组件样式示例: 1.2 原因分析(一) 时间单点选择定位下的区间功能缺失TextPicker 组件的核心定位是单一维度的选择工具,其设计初衷是满足用户对单个时间点(如某一具体日期、时刻)的快速选择需求。这种定位决定了组件在功能规划上更侧重单点选择的便捷性,而非复杂区间选择的完整性。导致其天然缺乏应对时间段选择场景的 “基因”。(二) 开发逻辑的独立性TextPicker 组件的底层实现逻辑具有较强的独立性与封闭性。每个组件实例仅负责处理自身对应的时间维度数据(如年、月、日或时、分),若要实现开始时间与结束时间的联动(如结束时间随开始时间动态调整),必须通过外部代码强制建立关联,这无疑增加了开发的复杂度,也容易因逻辑冲突导致功能异常。(三) 多组件分散:用户体验多重阻碍使用多个 TextPicker 组件进行时间段选择时,分散的交互模式会打破用户对时间选择的连贯认知,完整选段动作被拆分为多个独立步骤,导致用户注意力需多次聚焦,易因界面跳转产生认知中断,出现忘记数值或混淆组件功能的情况;步骤繁琐不仅增加操作难度,还需用户自行核对时间逻辑关系,易引发误操作,且因缺乏统一交互流程引导,用户需自行摸索操作顺序,加重认知负担,最终影响操作效率和使用体验。 1.3 解决思路(一) 组件整合:打造一体化时间段选择工具针对鸿蒙原生 TextPicker 组件在时间段区间选择场景中的痛点(功能缺失、开发低效、体验不佳),核心思路是基于组件化思想对原生组件进行封装扩展,通过复用原生能力、状态管理与动态适配,实现高效、直观的时间段区间选择功能。具体包括:Ø 复用 TextPicker 组件基础能力,组合单日期选择功能为 “日期 + 时间段” 的二级联动结构,解决区间选择功能缺失问题;Ø 采用鸿蒙状态管理机制(@State、@Link)维护父子组件状态同步,减少手动处理状态的开发成本,提升开发效率;Ø 结合动态时间规则处理,根据当前时间智能调整可选时段(如移除过期时段、跨天切换),优化用户操作体验,避免无效选择。​(二) 交互增强:提升操作连贯性与便捷性​支持在同一组件内通过拖拽、滑动等手势快速调整时间区间,比如拖动开始时间端点或结束时间端点来改变整个时间段,让操作更直观连贯。同时,保存用户的历史选择习惯,当用户再次进行时间段选择时,自动显示常用的时间范围作为初始选项,缩短选择路径。还可通过动画过渡效果增强两个时间点选择的关联性,例如选择开始时间后,结束时间区域以轻微高亮或缩放动画提示用户继续操作,强化操作的连贯性认知。​1.4 解决方案(一) 日期处理工具:通过工具函数实现日期格式化与动态时间规则适配,确保可选时段的有效性:日期格式化(getShiJian 函数):根据传入的天数偏移量(addDay),动态生成未来日期的 “YYYY-MM-DD” 格式,支持多日期选择。示例代码: getShiJian(addDay: number) { // 假设已经获取到时间戳并转换为Date对象 const date = new Date(Date.now() + (86400000 * addDay)); const year = date.getFullYear(); // 月份从0开始,需要加1并补0 const month = ("0" + (date.getMonth() + 1)).slice(-2); const day = ("0" + date.getDate()).slice(-2); // 日期补0 const formattedTime = `${year}-${month}-${day}` return formattedTime } timeRules(shiJian: Children[]) { const date: Date = new Date() const hours = date.getHours() if (hours >= 9 && hours < 12) { return } else if (hours >= 9 && hours < 11) { this.cascade[0].children?.splice(0, 1) } else if (hours >= 11 && hours < 16) { this.cascade[0].children?.splice(0, 2) } else if (hours >= 16 && hours < 22) { this.cascade[0].children?.splice(0, 3) } else if (hours < 8) { return } else if (hours >= 22) { this.cascade.splice(0, 1) this.cascade.push({ text: `${getShiJian(7)}`, children: shiJian }) } }动态时间规则(timeRules 函数):基于当前小时数(date.getHours ())智能调整可选时段。示例代码: if (hours >= 9 && hours < 12) { return } else if (hours >= 9 && hours < 11) { this.cascade[0].children?.splice(0, 1) } else if (hours >= 11 && hours < 16) { this.cascade[0].children?.splice(0, 2) } else if (hours >= 16 && hours < 22) { this.cascade[0].children?.splice(0, 3) } else if (hours < 8) { return } else if (hours >= 22) { this.cascade.splice(0, 1) this.cascade.push({ text: `${getShiJian(7)}`, children: shiJian }) }(二) 时间区间选择组件(TimeComponent):封装二级联动选择器,实现 “日期 + 时间段” 一体化选择:双级联动结构:通过TextPicker的range绑定cascade数组(包含日期及对应时段),实现联动选择。示例代码:@Component export struct TimeComponent { @Link selectTime: string; @Link cascade: TextCascadePickerRangeContent[] @Link isXuanZeShiJian: string @Link isXianShiShiJian: boolean shiJian: Children[] = [{ text: '08:00-09:00' }, { text: '09:00-11:00' }, { text: '11:00-16:00' }, { text: '16:00-22:00' }, ] aboutToAppear(): void { this.timeRules(this.shiJian) } build() { Scroll() { Column() { TextPicker({ range: this.cascade }) .onScrollStop((value: string | string[], index: number | number[]) => { console.info('TextPicker 多列联动:onScrollStop ' + JSON.stringify(value) + ', ' + 'index: ' + JSON.stringify(index)) this.selectTime = `${value[0]} ${value[1]}` }) .canLoop(false) .width('100%') .height(150) .margin({ bottom: 50 }) Row({ space: 20 }) { Button('取消') .width(120) .height(40) .buttonStyle(ButtonStyleMode.NORMAL) .onClick(() => { this.isXianShiShiJian = false }) Button('确定') .width(120) .height(40) .buttonStyle(ButtonStyleMode.EMPHASIZED) .onClick(() => { this.isXianShiShiJian = false if (this.selectTime) { this.isXuanZeShiJian = this.selectTime } else { this.isXuanZeShiJian = `${this.cascade[0].text} ${this.cascade[0]?.children![0].text}` } }) } .width('100%') .justifyContent(FlexAlign.Center) } .width('100%') .height('100%') .backgroundColor('#FFFFFF') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .translate({ y: -20 }) } .width('100%') .height('100%') } timeRules(shiJian: Children[]) { const date: Date = new Date() const hours = date.getHours() if (hours >= 9 && hours < 12) { return } else if (hours >= 9 && hours < 11) { this.cascade[0].children?.splice(0, 1) } else if (hours >= 11 && hours < 16) { this.cascade[0].children?.splice(0, 2) } else if (hours >= 16 && hours < 22) { this.cascade[0].children?.splice(0, 3) } else if (hours < 8) { return } else if (hours >= 22) { this.cascade.splice(0, 1) this.cascade.push({ text: `${getShiJian(7)}`, children: shiJian }) } } } interface Children { text: string; }状态管理:通过 @Link 装饰器同步父组件状态(如 cascade、selectTime),确保选择结果实时反馈。交互优化:默认取首个日期的首个时段(this.selectTime = cascade[0].text + cascade[0].children[0].text),避免空状态;通过 “取消”“确定” 按钮控制组件显隐(绑定 isShowTime 状态)。(三) 关键交互流程:用户操作流程:点击时间输入框→组件弹出→选择日期(联动加载对应时段)→选择时段→点击 “确定”→结果同步至父组件,单次操作完成区间选择。1.5   方案成果总结(一) 组件层面,通过组件一体化设计消除多组件切换的割裂感,减少用户手动校验成本,经测试,时间段选择效率提升约 40%;(二) 流程层面,清晰引导与快捷选项简化操作步骤,使操作步骤从原来的 3 - 4 步减少至 2 - 3 步,实时校验让选择错误率降低 65% 以上,显著降低用户认知负担;(三) 交互层面,手势操作与历史记忆功能缩短选择路径,平均选择耗时减少 30%,动画效果强化操作连贯性,用户操作满意度提升 50%,全面提升用户操作的直观性与便捷性,最终实现时间段选择体验的优化升级。
  • [技术干货] App Linking助力应用场景创新,操作步骤立省 60%
    在竞争激烈的应用市场中,开发者们都在努力寻找让应用脱颖而出的方法。华为AppGallery Connect(简称AGC)向开发者推出App Linking技术服务提供“应用链接”和“元服务链接”,可用于实现跳转HarmonyOS应用或者跳转元服务的功能。还能够与碰一碰、扫码等一方特性无缝结合,为应用构建独特的创新场景,有效提升应用的竞争力和用户体验。案例一:华为视频碰一碰,让跨设备视频分享一步到位​华为视频基于 App Linking 与碰一碰结合,只需两台手机轻触,视频瞬间直达对方应用播放界面,跳过传统社交分享的繁琐流程。无需手动搜索好友、上传等待或复制链接,精彩内容一碰即传,社交分享体验全面升级。 案例二:多乐中国象棋碰一碰,实现组队效率跃升 60%​多乐中国象棋引入 App Linking 与碰一碰的组合,为玩家带来了秒速组队的全新体验。以往玩家组队需要经过添加好友、发出邀请、等待回应等一系列繁琐操作,而现在轻轻碰触两台手机,即可实现秒速组队,操作步骤立省 60%。案例三:美团一扫即达,操作效率提升30%以上​美团App接入App Linking,无需用户打开App,通过系统扫码直接解锁共享单车、租借充电宝。负一屏、控制中心、系统相机均可解锁,操作入口增加3倍。一步扫码直达,操作效率提升30%以上。 App Linking 与一方特性的结合,为开发者打造创新应用场景提供了有力支持,无论是内容分享、游戏互动还是服务直达,都能带来显著的效果。它不仅能帮助开发者提升应用的竞争力,还能为用户带来更便捷、高效的使用体验。​点击下方链接,即刻开启鸿蒙生态场景化运营新篇章——点击链接即可体验:App Linking (上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
  • [技术干货] 元服务调测无需上架,App Linking助力高效分发元服务链接
           元服务已经开发完却无法进行效果测试,上架后又担心影响现网用户体验。为解决开发者在元服务调测场景中的核心痛点,App Linking 全新升级实用功能 —— 支持未上架元服务进行全阶段调测,无需等待上架且不影响现网,让链接运营更高效、用户体验更流畅!核心功能:1. 开发者配置App Linking元服务链接后,在开发、邀请测试等阶段均可使用元服务链接进行功能调测;2. 针对二维码测试链接,若元服务链接关联了二维码,可以单独创建测试链接进行端到端真机调测,确认无误后再正式发布到现网,避免对存量的二维码产生影响,可以有效提升元服务质量!具体操作步骤如下:1、创建元服务链接时可正常在AGC控制台进行配置,若不关联二维码,可直接使用元服务链接进行测试。2、若选择关联二维码,可创建测试链接进行调试,链接需符合匹配规则,多个测试链接使用英文分号分隔。3、点击“保存”后,元服务链接为“草稿态”,您可以查看、编辑或者删除链接信息。开发者可扫描由测试链接生成的二维码进行端到端调测。4、调测确认无误后可点击“发布”,AGC会判断当前时间是否在链接有效期内,并实时更新元服务链接状态:“待生效”、“已生效”或“已失效”。        App Linking元服务链接和融合码是华为AppGallery Connect(简称AGC)向鸿蒙开发者提供的链接跳转元服务能力,可用于通过点击链接或扫描存量二维码实现跳转元服务指定页面的功能,实现元服务的点击触达、即点即用,为鸿蒙用户带来便捷的体验。        对于开发者而言,App Linking 不只是简单的链接工具,更是提升用户使用体验的核心利器。它打通 “用户触达” 与 “服务落地”,让应用与用户连接更高效。点击下方链接,即刻开启鸿蒙生态场景化运营新篇章 ——App Linking 。        AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。        如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
  • [技术干货] 鸿蒙APMS:开箱即用,崩溃卡顿耗电秒级捕捉
    在移动应用生态中,用户体验直接决定产品成败。面对崩溃、卡顿、耗电等现网性能问题,华为AppGallery Connect(简称AGC)向开发者提供了一个现网质量监测解决方案—应用性能监测服务(Application Performance Management Service,简称APMS)——开箱即用,崩溃卡顿耗电秒级捕捉,守护应用全生命周期,为开发者打造免集成、全覆盖、实时化的质量监测方案。​​核心优势:四大优势助力性能监测​​l  ​​免集成,开箱即用​​APMS以鸿蒙系统服务形式运行,​​无需应用内嵌SDK或初始化组件​​,开通即可启用,大幅降低接入成本。l  ​​全生命周期覆盖​​APMS的异常信息采集动作不受应用初始化、组件加载影响,全面覆盖应用启动、运行、前/后台、异常终止各阶段、状态的异常。l  ​​实时上报与精准统计​​通过系统级服务独立执行数据上传,​​秒级捕捉崩溃、ANR(应用无响应)、帧率异常等问题​​,并提供环境上下文、完整堆栈轨迹及多维度分析报表。异常信息上传动作由系统服务执行,与应用运行状态无关,不依赖应用重新启动,实时上报。l  ​​多维分析灵活查询​​可提供每个问题发生时的环境信息、堆栈信息等分析数据,并支持基于堆栈关键行进行准确的同类异常汇聚,支持问题标记、指标告警等辅助能力,帮助开发者高效聚焦和解决高频问题。​​案例展示: 助力游戏应用崩溃率从0.48%优化至0.35%(降幅27%)​​ 某游戏应用接入APMS服务后,依托其开箱即用的便捷性、全程守护及快速响应的高效性,关键指标实现显著改善:应用崩溃率从0.48%优化至0.35%(降幅27%),7日内触发崩溃的独立用户数环比下降52%。APMS提供的实时监测数据与异常根因分析报告,助力开发团队高效闭环问题,持续夯实用户体验基线。点击链接即可体验:APMS业务。(上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。  如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
  • [技术干货] 「软件包管理 + 上架自检」双管齐下,提升上架审核通过率
    还在为应用上架反复碰壁而头疼?别担心,HUAWEI AppGallery Connect 云测试推出的“软件包管理--上架自检”能力,通过前置合规校验、模拟真实环境测试并出具详细的测试报告。平台配置多种型号鸿蒙真机,覆盖主流/热门机型,满足多样化测试场景需求。每天提供300分钟的免费使用时间,足够支撑新手尝鲜、轻量级项目测试或多次验证,​​0成本起步测试,立省真机购买投入!一、解锁新技能:这个 “应用管家” 不一般​全新上线的“软件包管理”能力,从应用提交伊始即提供全流程辅助,像一个贴心的私人“应用管家”。开发者上传软件包后,系统将​​自动进行基础合法性检测​​,校验其是否符合鸿蒙生态基本规范。支持开发者按需选择“仅测试”或“测试及正式上架”场景,满足不同阶段的验证需求。上传后即刻获取软件包合规状态(“已达标” “待优化”或“不通过”),清晰呈现应用与上架基础要求的差距。更为核心的是“上架自检”功能:严格参照华为应用市场准入标准,在热门的移动终端设备上,对应用的​​兼容性、稳定性、性能、功耗及用户体验(UX)​​ 进行全面自动化检测,让开发者在上架前及时发现并修复问题。每次检测均生成详尽报告,明确罗列问题点并提供优化建议,大幅减少反复修改的成本和时间。​二、操作指南:三步高效管理软件包​​​​1.登录配置:​​ 登录 ​​AppGallery Connect​​,点击“APP”,选择要发布的应用。​​导航入口:​​ 在左侧导航栏选择 ​​“应用上架 > 软件包管理”​​。​​2.上传与确认:​​ 点击右上角“上传”,选择应用使用场景(“仅测试”或“测试和正式上架”),添加软件包文件。  ​​3.结果解读:​​上传成功后,页面生成软件包记录。您可在“合法性”栏查看合法性检测结果。l  ​​已达标:​​ 符合鸿蒙生态规范,可提交上架。点击“报告”可查看详细的检测报告。l  ​​待优化:​​ 可尝试提交上架,但存在驳回风险。建议点击“报告”查看并修复问题。l  ​​不通过:​​ 不满足上架基本要求,不允许上架,需根据报告建议修改后重新上传。  三、进阶技巧:上架自检,为应用质量加码强烈建议开发者充分利用​​“上架自检”​​功能,为应用质量增加一层保障。1.在软件包记录的“操作”列点击“启动自检”。​​请注意:​​ 每个应用同时仅允许一个自检任务运行;即使删除相关软件包,启动自检仍会计入服务使用配额。2.启动后在“上架自检”栏可查看状态及结果:l  ​​检测中:表示正在检测软件包,检测时长可能受终端设备数量和排队情况影响。l  通过:​​ 可安心提交上架申请。l  ​​待优化:​​ 虽可提交,但驳回风险较高,强烈建议按报告指引进行优化。  HUAWEI AppGallery Connect 云测试提供​​海量鸿蒙真机​​在线免预约,可全面检测应用兼容性、性能、稳定性、功耗及UX等关键指标,​​首次试用赠300分钟​​!轻量测试0成本起步,极简操作,高效输出报告。成本低、易上手,​​点此立即试用 >>​ AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。  如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
  • [分享交流] 深圳人才福利!5大“国家队”级信创技术免费培训课程开始报名
    当别人已在鸿蒙生态接单月入3万+,你还在Java内卷?当大模型重构IT岗位,传统运维正批量淘汰…体贴的深圳人社为您再次奉上信创技术/鸿蒙系统/麒麟系统等新一代信息应用技术生存技能大餐•       信创智算与大模型技术课程•       开源高斯数据库技术课程•       鸿蒙原生应用开发课程•       开源鸿蒙设备应用技术课程•       前沿  科技  国产系统应用 还等什么基于麒麟操作系统的信创基础软件适配迁移与运维课程 【理论授课、现场实战、组队攻坚】告别枯燥的理论推砌聘请行业专家担任讲师5门免费课程助你抢占信息技术高地,满级晋升!最重要的是全!免!费!咱不花一分钱就开启成长与蜕变的大门报名有啥要求?咋报名?快随我往下看吧!一、  报名条件报名学员需具有新一代电子信息应用技术相关的行业从业背景或具备相关专业背景,并满足以下条件之一即可:1.本市户籍人员;2.在本市正常缴交社会保险的人员;3.深圳市登记失业人员;4.在深圳市公共就业服务机构进行离校两年未就业实名登记的本市高校毕业生;5.本市高校或本市户籍在市外高校的毕业年度(指毕业时间在2025年1月1日至12月31日之间)毕业生(含技师学院高级工班、预备技师班)。温馨提示:(1)同一劳动者同一年度只能参加1次项目制培训哦。如果您已经参加2024年度项目制培训但未完成规定学时50%以上,很遗憾,那无法参加2025年度的项目制培训了哦。(2)同一劳动者同一年度内企业新型学徒制培训,学生学徒制培训、技培生学徒制培训只能参加一次,且均不能和项目制培训同时享受。等等先别急还没完!还有额外补贴!【额外补贴】如您满足以下条件之一:•       本市就业困难人员•       本市零就业家庭成员•       本市就业残疾人•       本市城市低保家庭成员•       本市毕业2年内的“两后生”中的农村学员•       本市求职就业的脱贫人员不仅可以免费学课程还可以再领500元的生活补贴金💴! 接下来咱们看一下具体学习内容吧!二、  学习内容以及报名方式 新一代电子信息应用技术项目制培训 指导单位:深圳市人力资源和社会保障局主办单位 :深圳市职业技能培训指导中心承办单位:深圳职业技术大学1、信创智算与大模型技术课程课程内容培训天数培训名额主要内容包括:1.基于昇腾平台的 DeepSeek 模型搭建与优化2.华为云昇腾算力支持下的私有场景大模型部署3.基于昇腾与 DeepSeek 的私有大模型自主训练4.电商场景下大模型的创新应用与拓展6天约300人 信创智算与大模型技术课程报名二维码及交流群(QQ群)    2、开源高斯数据库技术课程课程内容培训天数培训名额主要内容包括:1.高斯数据库安装与对象管理实操2.场景化高斯数据库实验探索3.数据库AI策略与技巧4.数据安全管理与防护6天约250人开源高斯数据库技术课程报名二维码及交流群(QQ群)   3、      鸿蒙原生应用开发课程课程内容培训天数培训名额主要内容包括:1.基于ArkTSUI框架搭建实训云平台2.鸿蒙原生办公签到系统开发3.基于Next版本开发实时社交软件联动DeepSeek实现聊天问答4.基于鸿蒙服务卡片开发音乐推荐软件5.鸿蒙原生健康服务检测软件开发6天约250人 鸿蒙原生应用开发课程报名二维码及交流群(QQ群)     4、      开源鸿蒙设备应用技术课程课程内容培训天数培训名额主要内容包括:1.OpenHarmony搭建与配置2.开源鸿蒙设备驱动开发和集成3.基于开源鸿蒙的HAL层开发4.基于开源鸿蒙的智能家居软硬件开发6天约250人 开源鸿蒙设备应用技术课程报名二维码及交流群(QQ群)    5、基于麒麟操作系统的信创基础软件适配迁移与运维课程课程内容培训天数培训名额主要内容包括:1.银河麒麟桌面操作系统 V10 的管理应用2.掌握适配测试基础及软硬件适配测试技能6天约250人 基于麒麟操作系统的信创基础软件适配迁移与运维课程报名二维码及交流群(QQ群)   三、成长与收获1.掌握实用技能,提高自身职业技能,增强就业竞争力,优化职业发展路径;2.培训考勤达标且考核通过将获得《深圳市职业技能提升培训合格证书》;3.可自行选考行业权威认证:HCIA-AI认证、HCIA-openGauss认证、HarmonyOS应用开发者认证、开放原子OpenHarmony人才认证、KYCA、KYCP认证(不含考证费)。四、班级设置l  7月29日-9月30日  开设日常精品班(周一到周六)开设周末精品班(周六或周日单日班)l  7月29日-10月20日开设周末精品班(周六或周日单日班)五、咨询与联络黄老师:13528095312(微信同号)周老师:0755-26019607咨询时间:工作日9:00- 18:00其他时间咨询联系QQ群工作人员六、培训地点深圳职业技术大学西丽湖园区(信息楼)建议绿色出行:深圳地铁5号线西丽地铁站F口步行800米。公交车站-深圳职业技术大学(西丽湖园区),线路包括M197、M182、M176、M492、高峰专线59、325、M535、M217、67、326等。 别再犹豫,抓住这个难得的机会,让自己在公益性培训中实现华丽转身!立即报名,开启你的成长之旅!
  • “秒开”时代,HarmonyOS预加载让页面加载快如闪电
    在用户体验为王的时代,应用“秒开”已成为用户对移动应用的核心期待。HarmonyOS预加载服务将应用页面的数据提前加载到本地缓存,页面打开直接从本地获取数据渲染,有效提升页面打开速度,解决应用页面加载慢、白屏的困境。一、安装预加载:助力“凯叔讲故事”页面加载提速37.9%​安装预加载服务适用于安装后首次打开,应用首页加载提速场景。在应用安装时,将一些必要的资源,例如图片、音频、视频或数据文件,提前加载到本地进行缓存。当用户安装后首次打开应用时,直接调用本地缓存数据渲染页面,跳过网络请求环节。优质儿童内容品牌“凯叔讲故事”非常重视用户体验,接入安装预加载后,页面加载提速37.9%。  实际案例应用:凯叔讲故事 二、周期性预加载:动态场景即发即现​​周期性预加载服务适用于任意页面加载提速的场景,可与安装预加载结合使用。系统每隔12小时拉取一次指定页面(不局限首开页面)的云侧数据并将其缓存到本地,并可将静态资源放置到云端,减少包体大小。开发者可在节日活动开始前通过周期性预加载服务提前将主题资源获取到本地,用户访问时直接从本地获取即可,减少了网络请求的时间和带宽消耗,实现即发即现的效果,提升用户体验。三、FastWeb:移动端H5页面的“隐形加速器”FastWeb组件是一个高性能Web容器,针对App中H5页面可以提供预启动、预渲染、预编译、离线资源免拦截注入等能力。其可提前加载高频页面资源并优化渲染流程,有效解决传统H5页面的白屏、卡顿问题。某省政务服务App接入FastWeb后,页面打开速度提升超32.9%。  (上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。         如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
总条数:446 到第
上滑加载中