HarmonyOS 6商城开发学习:权限申请的正确姿势——如何区分首次申请与用户已拒绝
熟悉我们购物比价应用的朋友都知道,商城应用离不开各种权限:扫码需要相机权限、定位附近门店需要位置权限、保存商品图片需要存储权限……权限申请是每个应用必经的门槛。但这里面有个老大难问题:用户第一次拒绝权限后,第二次再申请时,系统弹窗的行为完全不同——有些设备会直接静默拒绝,连弹窗都不给。这时候如果应用还傻乎乎地继续弹窗,用户会觉得“这应用怎么回事,一直弹窗烦死了”。
我们之前就踩过这个坑。用户拒绝了相机权限,下次扫码时我们又调了一次requestPermissionsFromUser,结果系统根本没弹窗,直接返回拒绝,我们还以为用户又点了一次“拒绝”,实际上系统已经记住了用户的决定,不再打扰用户了。后来我们仔细研究了华为的权限API,才发现PermissionRequestResult里有个dialogShownResults字段,可以判断这次弹窗是否真的弹出来了。这篇文章完整记录一下实现过程和踩坑经验。
功能设计
先说说预期效果。
用户在商城应用中首次需要使用某个权限(比如相机扫码)时,应用弹窗请求授权。如果用户点击“允许”,一切正常。如果用户点击“拒绝”,下次再次触发同一权限请求时,应用不应该再次弹窗(因为系统不会再弹了),而是应该引导用户去系统设置中手动开启。同时,应用需要能区分以下几种情况:
-
首次申请:系统弹窗,用户可选择允许或拒绝。
-
用户已拒绝但未勾选“不再询问”:再次申请时系统还会弹窗(部分系统行为)。
-
用户已拒绝且勾选了“不再询问”:再次申请时系统不弹窗,直接返回拒绝。
-
权限已被授予:无需任何操作。
核心目标:
-
准确判断当前权限状态是“从未申请过”还是“已被用户拒绝”。
-
在系统不再弹窗时,优雅地引导用户去设置页面手动开启。
-
避免重复弹窗骚扰用户。
核心API
|
API/接口 |
说明 |
|---|---|
|
|
同步检查权限授权状态,返回 |
|
|
拉起系统弹框请求用户授权,返回 |
|
|
布尔数组,对应每个权限的弹窗是否真正显示。 |
关键点:checkAccessTokenSync只能告诉你当前是授权还是拒绝,但无法区分“从未申请”和“已拒绝”。而dialogShownResults可以告诉你本次弹窗是否真的弹出——如果没弹出,说明系统已经记住了用户的拒绝决定,不能再指望弹窗了。
实现过程
封装权限管理器
首先,我们封装一个通用的权限管理器,统一处理权限检查和申请逻辑。
// utils/PermissionManager.ets
import { abilityAccessCtrl, AtManager } from '@kit.AbilityKit';
import { BusinessError } from '@ohos.base';
import { common } from '@kit.AbilityKit';
export class PermissionManager {
private atManager: AtManager;
private context: common.Context;
constructor(context: common.Context) {
this.atManager = abilityAccessCtrl.createAtManager();
this.context = context;
}
/**
* 检查权限是否已授予
*/
checkPermission(permission: string): boolean {
const grantStatus = this.atManager.checkAccessTokenSync(
this.context.applicationInfo.accessTokenId,
permission
);
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
/**
* 请求权限,返回详细结果
*/
async requestPermission(permission: string): Promise<PermissionResult> {
// 1. 先检查当前状态
const isGranted = this.checkPermission(permission);
if (isGranted) {
return { granted: true, dialogShown: false, needGuide: false };
}
// 2. 尝试弹窗请求
try {
const result: PermissionRequestResult = await this.atManager.requestPermissionsFromUser(
this.context,
[permission]
);
const dialogShown = result.dialogShownResults?.[0] ?? false;
const granted = result.authResults?.[0] === 0;
// 核心判断逻辑
if (!dialogShown && !granted) {
// 系统没有弹窗,且权限未被授予 → 说明用户之前拒绝过且系统不再弹窗
return { granted: false, dialogShown: false, needGuide: true };
}
return { granted, dialogShown, needGuide: false };
} catch (error) {
console.error(`请求权限失败: ${JSON.stringify(error)}`);
return { granted: false, dialogShown: false, needGuide: true };
}
}
/**
* 跳转到系统设置页(引导用户手动开启权限)
*/
async navigateToAppSetting(): Promise<void> {
try {
const bundleName = this.context.applicationInfo.bundleName;
await this.context.startAbility({
bundleName: 'com.huawei.hmos.settings',
abilityName: 'com.huawei.hmos.settings.MainAbility',
parameters: {
pushParams: bundleName
}
});
} catch (error) {
console.error(`跳转设置页失败: ${JSON.stringify(error)}`);
}
}
}
// 返回结果类型
interface PermissionResult {
granted: boolean; // 是否已授权
dialogShown: boolean; // 本次弹窗是否显示
needGuide: boolean; // 是否需要引导用户去设置
}
在扫码页面中使用
以相机权限为例,在扫码页面中调用:
// view/ScanPage.ets
import { PermissionManager } from '../utils/PermissionManager';
@Component
export struct ScanPage {
private permissionManager: PermissionManager = new PermissionManager(getContext(this));
@State showGuideDialog: boolean = false;
async requestCameraPermission(): Promise<boolean> {
const result = await this.permissionManager.requestPermission('ohos.permission.CAMERA');
if (result.granted) {
// 权限已获取,开始扫码
this.startScanning();
return true;
}
if (result.needGuide) {
// 需要引导用户去设置
this.showGuideDialog = true;
return false;
}
// 弹窗显示了但用户拒绝了
// 可以再次提示用户,但不要立刻再次弹窗
this.showDeniedTip();
return false;
}
build() {
Column() {
Button('扫码')
.onClick(() => this.requestCameraPermission())
if (this.showGuideDialog) {
// 引导弹窗
AlertDialog.show({
title: '需要相机权限',
message: '请在系统设置中开启相机权限,以便扫码功能正常使用',
primaryButton: {
value: '去设置',
action: () => {
this.permissionManager.navigateToAppSetting();
this.showGuideDialog = false;
}
},
secondaryButton: {
value: '取消',
action: () => {
this.showGuideDialog = false;
}
}
})
}
}
}
}
完整的权限状态判断流程
为了更清晰地展示逻辑,画个伪代码流程图:
function handlePermission(permission):
if checkPermission(permission) == GRANTED:
return OK
result = requestPermissionsFromUser([permission])
if result.dialogShownResults[0] == true:
// 弹窗显示了
if result.authResults[0] == 0:
return OK // 用户同意了
else:
return DENIED_BUT_CAN_RETRY // 用户拒绝了,但下次还能弹窗
else:
// 弹窗没显示(系统阻止了)
if result.authResults[0] == 0:
// 理论上不可能,但以防万一
return OK
else:
return NEED_GUIDE_TO_SETTINGS // 必须引导去设置
遇到的问题与解决方案
问题1:dialogShownResults在某些机型上返回 undefined
一开始我们直接取 result.dialogShownResults[0],结果在某些低版本设备上 crash 了。原因是 dialogShownResults可能为 undefined。解决方案:加安全取值。
const dialogShown = result.dialogShownResults?.[0] ?? false;
问题2:用户拒绝后再次弹窗,系统仍然弹窗,但我们想避免重复骚扰
有些系统版本中,用户拒绝后再次申请,系统仍然会弹窗(但会带上“不再询问”复选框)。这种情况下,如果我们不加控制,每次用户触发扫码都会弹窗,体验很差。解决方案:记录用户拒绝的次数,如果超过一定次数(比如2次),就不再弹窗,直接引导去设置。
// 在PermissionManager中添加计数器
private denyCount: Map<string, number> = new Map();
async requestPermission(permission: string): Promise<PermissionResult> {
// ...
if (!granted) {
const count = this.denyCount.get(permission) || 0;
this.denyCount.set(permission, count + 1);
if (count >= 2) {
// 拒绝次数过多,直接引导
return { granted: false, dialogShown: false, needGuide: true };
}
}
// ...
}
问题3:跳转系统设置页的URI在不同设备上不一致
我们最初写死的 bundleName和 abilityName在部分设备上无法跳转。后来发现应该使用 want的 parameters传递包名,系统会自动处理。更稳妥的做法是使用 UIAbilityContext.startAbility并传入 applicationInfo相关参数。上面代码中的实现是经过验证可用的。
总结
权限申请是商城应用的基础能力,但做好并不容易。核心要点总结如下:
|
要点 |
实现方式 |
|---|---|
|
检查权限状态 |
|
|
请求权限 |
|
|
判断是否弹窗 |
|
|
引导用户去设置 |
|
|
避免重复骚扰 |
记录拒绝次数,超过阈值直接引导 |
改完之后,我们的扫码功能再也不会莫名其妙地反复弹窗了。用户第一次拒绝,我们友好提示;用户再次拒绝,我们直接引导去设置。整个体验顺畅了很多,应用商店的评分也回升了。如果你也在做购物比价类应用,不妨试试这套权限管理方案。
更多推荐



所有评论(0)