鸿蒙应用开发实战【10】— HarmonyOS NEXT 开发 10 大常见坑与最佳实践

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里插入图片描述

经过号码助手项目的完整开发周期,我们踩过了大大小小十余个坑,有的让页面变成黄色、有的让按钮毫无反应、有的让编译永远失败。本篇将这些真实踩过的坑完整记录下来,每个坑都附有错误现象、根本原因、正确修复代码,帮助你少走弯路,直接写出稳定、正确的 HarmonyOS NEXT 代码。

本篇涵盖:#AARRGGBB 颜色格式、async onClick 静默失败、子组件 router 导航陷阱、@Builder 变量声明限制、interface 重名、ForEach key 函数、bindSheet 参数、Emoji 图标颜色不受控等 10 大典型坑。


一、坑 #1:颜色格式错误导致卡片变黄

1.1 问题现象

统计卡片和列表卡片全部显示为淡黄色,而设计稿要求是半透明白色。

1.2 根本原因

ArkUI 使用 #AARRGGBB 格式(Alpha 在最前),不是 CSS 的 #RRGGBBAA

// ❌ 错误:CSS 习惯写法,Alpha 在最后
static readonly CARD: string = '#FFFFFFB8'
// ArkUI 解析:AA=0xFF(不透明), R=0xFF, G=0xFF, B=0xB8
// 结果:RGB(255,255,184) = 淡黄色!

// ✅ 正确:#AARRGGBB,Alpha 在最前
static readonly CARD: string = '#B8FFFFFF'
// ArkUI 解析:AA=0xB8(72%不透明), R=0xFF, G=0xFF, B=0xFF
// 结果:72% 透明度的白色

快速换算表

不透明度 AA 值 示例(白色)
100% FF #FFFFFFFF
90% E6 #E6FFFFFF
72% B8 #B8FFFFFF
50% 80 #80FFFFFF
8% 14 #14FFFFFF

二、坑 #2:async onClick 静默失败

2.1 问题现象

点击「添加应用」按钮,页面完全没有反应,也没有任何错误提示。

2.2 根本原因

// ❌ 错误:async 回调无 try/catch,Promise rejection 被静默吞掉
.onClick(async () => {
  const cards = await CardDao.listAll()   // 如果 DB 未就绪,这里抛异常
  if (cards.length === 0) {
    router.pushUrl({ url: 'pages/CardManagePage' })
    return
  }
  router.pushUrl({ url: 'pages/AddAppPage' })
  // ↑ 如果 CardDao.listAll() 失败,这里永远不会执行
  // Promise rejection 没有 catch,被系统静默吞掉
  // 用户看到:按钮毫无反应
})

2.3 正确写法

// ✅ 方案一:提取 async 方法,onClick 不用 async
.onClick(() => {
  // 直接使用已加载的 this.cards 状态(onPageShow 时已加载)
  if (this.cards.length === 0) {
    this.navigate('pages/CardManagePage')
    return
  }
  this.navigate('pages/AddAppPage')
})

// ✅ 方案二:必须用 async 时,加完整 try/catch
.onClick(async () => {
  try {
    const cards = await CardDao.listAll()
    if (cards.length === 0) {
      this.navigate('pages/CardManagePage')
      return
    }
    this.navigate('pages/AddAppPage')
  } catch (e) {
    promptAction.showToast({ message: '操作失败,请重试' })
  }
})

三、坑 #3:子组件中 router 导航静默失败

3.1 问题现象

AppListItem 是子组件(非 @Entry),点击列表项跳转详情页,毫无反应。

3.2 根本原因

// ❌ 错误:在子组件中调用 this.getUIContext().getRouter()
// this.getUIContext() 在某些时机可能同步抛异常
// .catch() 只捕获 Promise rejection,不捕获同步异常
.onClick(() => {
  this.getUIContext().getRouter().pushUrl({  // ← 可能同步抛异常
    url: 'pages/AppDetailPage',
    params: { bindingId: this.bindingId }
  }).then(() => {
    console.info('成功')
  }).catch((err: Error) => {
    console.error(err.message)  // ← 捕获不到同步异常
  })
})

3.3 正确写法

// ✅ 正确:直接 import router,用 try/catch + callback 形式
import { router } from '@kit.ArkUI'

.onClick(() => {
  try {
    AppStorage.setOrCreate<number>('current_binding_id', this.bindingId)
    router.pushUrl(
      { url: 'pages/AppDetailPage', params: { bindingId: this.bindingId } },
      router.RouterMode.Standard,
      (err: Error) => {
        if (err) {
          promptAction.showToast({ message: `打开失败:${err.message}` })
        }
      }
    )
  } catch (e) {
    promptAction.showToast({ message: '打开失败,请重试' })
  }
})

核心原则:在任何组件(无论是否 @Entry)中导航,都应使用 import { router } 直接调用,避免使用 this.getUIContext().getRouter(),并用 callback 形式捕获异步错误。


四、坑 #4:@Builder 方法内不能声明变量

4.1 问题现象

编译报错:@Builder method cannot contain variable declarations

4.2 根本原因

// ❌ 错误:@Builder 内部使用 const/let 声明变量
@Builder
private StatCard(icon: string, num: number, label: string, status: string) {
  const bgColor = status === '待换绑' ? AppColors.WARN_BG : AppColors.DANGER_BG  // ← 编译错误!
  Column() {
    Text(icon).backgroundColor(bgColor)
  }
}

4.3 正确写法

// ✅ 方案一:把逻辑计算提到 private 方法
private getStatCardBg(status: string): string {
  return status === '待换绑' ? AppColors.WARN_BG : AppColors.DANGER_BG
}

@Builder
private StatCard(icon: string, num: number, label: string, bg: string, fg: string) {
  // @Builder 内只有纯 UI 描述,无变量声明
  Column() {
    Text(icon).backgroundColor(bg)
  }
}

// ✅ 方案二:把所有参数直接作为 @Builder 参数传入
this.StatCard('↔', 3, '待换绑', AppColors.WARN_BG, AppColors.WARN)

五、坑 #5:interface 不支持声明合并

5.1 问题现象

编译报错:Declaration merging is not supported

5.2 根本原因

// ❌ 错误:同一文件内两个同名 interface(ArkTS 不支持合并)
interface MenuButton {
  text: string
}
// ... 大量代码 ...
interface MenuButton {        // ← 重复声明,编译错误!
  color: string | Resource
}

典型场景:使用 Write 工具创建文件时,新旧内容追加导致同名 interface 重复出现。

5.3 正确写法

// ✅ 合并为同一个 interface,或改名
interface AddAppMenuButton {
  text: string
  color: string | Resource
}

六、坑 #6:Emoji 图标颜色不受 fontColor 控制

6.1 问题现象

统计卡片中的图标 🗑(垃圾桶)和 💼(公文包),设置 fontColor 无效,始终显示彩色 Emoji。

6.2 根本原因

Unicode Emoji(如 🗑 U+1F5D1)有内置的彩色渲染,CSS/ArkUI 的 fontColor 无法覆盖 Emoji 的颜色。

6.3 正确写法

// ❌ Emoji:fontColor 无效
this.StatCard('🗑', this.pendingCancel, '待注销', ...)

// ✅ 使用普通 Unicode 符号(fontColor 有效)
this.StatCard('×', this.pendingCancel, '待注销', AppColors.DANGER_BG, AppColors.DANGER)
this.StatCard('–', this.disabledCount, '已停用', AppColors.MUTED_BG, AppColors.MUTED)
this.StatCard('↔', this.pendingSwitch, '待换绑', AppColors.WARN_BG, AppColors.WARN)

七、坑 #7:bindSheet 参数类型错误

7.1 问题现象

编译报错:Argument of type '{ detents: ..., borderRadius: ... }' is not assignable

7.2 根本原因

// ❌ 错误:detents 和 borderRadius 参数类型不符
.bindSheet($$this.showCardSheet, this.CardPickerSheet, {
  detents: [SheetSize.FIT_CONTENT],        // ← detents 类型与实际 API 不符
  borderRadius: { topLeft: 20, topRight: 20 }  // ← 不支持分角设置
})

7.3 正确写法

// ✅ 正确:使用 height 替代 detents,去掉 borderRadius
.bindSheet($$this.showCardSheet, this.CardPickerSheet, {
  height: SheetSize.FIT_CONTENT,    // ✅ 高度自适应内容
  preferType: SheetType.BOTTOM,     // ✅ 从底部弹出
  showClose: false,
  dragBar: true,
  backgroundColor: '#FFFFFF'
})

八、坑 #8:FAB 按钮在空状态时重复显示

8.1 问题现象

首页空状态时,内嵌了一个「添加应用」按钮,同时 overlay FAB 也显示了「添加应用」按钮,导致页面有两个相同按钮。

8.2 正确写法

// ✅ FAB 只在有列表时显示(空状态已有内嵌按钮)
@Builder
private FabOverlay() {
  if (!this.selectMode && this.rows.length > 0) {   // ← 关键:rows.length > 0
    Row({ space: 10 }) {
      // FAB 按钮
    }
  }
  // rows.length === 0 时不渲染,避免与空状态内嵌按钮重复
}

九、坑 #9:ForEach key 函数使用 index 导致列表闪烁

9.1 问题现象

删除列表中间的某一项时,后面所有列表项都重新渲染(闪烁)。

9.2 根本原因

// ❌ 错误:以 index 为 key,删除中间项后所有 index 变化,全部重渲染
ForEach(this.rows, (row: AppRow) => {
  AppListItem({ ... })
}, (row: AppRow, index: number) => `${index}`)

9.3 正确写法

// ✅ 正确:以数据唯一标识为 key,只重渲染变化的项
ForEach(this.rows, (row: AppRow) => {
  AppListItem({ ... })
}, (row: AppRow) => `binding_${row.binding.id ?? 0}_${row.binding.status}`)
// 包含 id(保证唯一)和 status(影响显示的状态)

十、坑 #10:@State 数组深层修改不触发 UI 刷新

10.1 问题现象

调用 this.selectedIds.push(id) 后,UI 没有更新(按钮状态未变化)。

10.2 根本原因

@State 基于引用变化触发刷新,push 不改变数组引用,所以不触发。

// ❌ 不触发刷新:直接修改数组(引用未变)
this.selectedIds.push(newId)          // ❌
this.selectedIds.splice(0, 1)         // ❌

// ✅ 触发刷新:替换数组引用
this.selectedIds = this.selectedIds.concat([newId])           // ✅ 新增
this.selectedIds = this.selectedIds.filter(i => i !== oldId)  // ✅ 删除
this.selectedIds = [...this.selectedIds]                       // ✅ 强制刷新

十一、最佳实践清单

11.1 导航最佳实践

// ✅ 统一封装 navigate 方法
private navigate(url: string, params?: Object): void {
  try {
    router.pushUrl({ url, params }, router.RouterMode.Standard, (err: Error) => {
      if (err) promptAction.showToast({ message: `打开失败:${err.message}` })
    })
  } catch (e) {
    promptAction.showToast({ message: '页面打开失败,请重试' })
  }
}

11.2 异步加载最佳实践

// ✅ 三态 UI + finally 确保 loading 关闭
private async loadData(): Promise<void> {
  this.loading = true
  try {
    this.data = await SomeDao.listAll()
  } catch (e) {
    this.loadError = true
    console.error('[Page] loadData:', JSON.stringify(e))
  } finally {
    this.loading = false  // 必须在 finally 中
  }
}

11.3 颜色定义最佳实践

// ✅ 统一在 AppColors 中定义,使用 #AARRGGBB 格式
static readonly CARD: string = '#B8FFFFFF'   // Alpha 在前!
static readonly LINE: string = '#14141E14'   // 8% 透明度黑色

十二、本篇小结

坑编号 问题 解决方案
#1 颜色变黄 #AARRGGBB 格式,Alpha 在前
#2 async onClick 无反应 提取 async 方法 或 完整 try/catch
#3 子组件导航无反应 import router + try/catch + callback
#4 @Builder 编译错误 逻辑提到 private 方法,@Builder 只写 UI
#5 interface 重名 合并或改名
#6 Emoji 颜色失控 改用普通 Unicode 符号
#7 bindSheet 类型错误 用 height 替代 detents
#8 FAB 重复显示 rows.length > 0 条件控制
#9 ForEach 列表闪烁 以数据 id 而非 index 作为 key
#10 数组修改不刷新 concat/filter 替换引用,而非 push/splice

参考资料

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐