老读者应该知道,我之前写过几篇 HarmonyOS 应用开发相关的文章,从环境搭建到基础组件都聊过。今天这篇是“之四”,专门聊页面构建。倒不是说之前的文章不重要,而是页面构建这一步,直接决定了你应用长什么样、用户用起来顺不顺手,是整个 UI 开发的核心骨架。如果你已经能跑起一个 Hello World,但还没搞明白页面到底是怎么组织起来的,那这篇就是为你准备的。还没入门的同学也不用急,文里会从最基础的项目结构讲起,一步一步带你把一个多页面应用搭起来。

1. 页面构建的整体思路与设计拆解

1.1 为什么 HarmonyOS 选择声明式 UI

HarmonyOS 应用开发现在主推的是 ArkUI 声明式开发范式。很多从 Android 或 iOS 转过来的同学,一开始最不习惯的就是这个“声明式”三个字。

传统 Android 开发里,你用 XML 画界面,然后用 findViewById 找到控件,再在代码里手动改控件属性,比如 setText、setVisibility。这种叫命令式编程,每一步都明确告诉系统“你要干嘛”。而 ArkUI 的思路反过来了,你只需要描述“界面长什么样”,至于怎么把这个描述变成真正的界面,系统替你操心。

打个比方,命令式 UI 就像是给装修工人列清单:“先砌墙、再铺电线、然后刷漆、最后装灯”,每一步都得盯着。而声明式 UI 就像给设计师一张效果图:“我要一个三室一厅,现代简约风”,剩下的交给设计师去实现。

ArkUI 的好处在于,当数据变化时,界面会自动更新,不用你手动去操作 UI 组件。这个特性在做复杂业务的时候特别爽,你只管改数据,界面自己会跟着变,省掉了一大堆样板代码。

1.2 页面构建的核心要素拆解

在 ArkUI 里构建一个页面,你绕不开三样东西:组件、布局、状态管理。

组件就是界面上的元素,比如文本 Text、按钮 Button、图片 Image、输入框 TextInput 这些。就好比盖房子用的砖、瓦、水泥、钢筋。

布局决定这些组件怎么排列。是按行排(Row)、按列排(Column)、还是叠在一起(Stack),或者网格排列(Grid)。布局选对了,页面结构才能稳,不同屏幕尺寸下才不会乱。

状态管理则是声动 UI 的灵魂。你定义一个变量,这个变量一变,依赖它的界面组件就自动刷新。有点像 Excel 里的公式,你改了单元格的值,所有引用这个单元格的公式结果都会跟着更新。

这三者配合起来,就是一个页面构建的全貌了。接下来的内容,就是围绕着如何把这三种要素组合出一个能跑、好看、又容易维护的应用界面展开。

2. 动手前的准备:工程结构与大方向

2.1 新建项目的目录结构解读

如果你已经用 DevEco Studio 建过一个空项目,会看到默认生成了一堆目录和文件。很多新手一看到这堆东西就头皮发麻,其实没那么复杂。

以标准 Stage 模型项目为例,关键目录是这样的:

  • entry:应用的主模块,一般你的页面代码都在这里
  • entry/src/main/ets:存放 ArkTS 代码的位置
    • entryability:Ability 相关代码,可以理解为应用入口
    • pages:页面文件所在地,默认有个 Index.ets 就是第一个页面
    • entry/src/main/resources:资源目录,图片、字符串、颜色都放这

还有一个重要的文件是 module.json5,它配置了应用的基本信息,比如入口 Ability 是哪个、权限声明、支持的设备类型等。

拿盖楼来类比的话,module.json5 相当于整个工程的“规划许可证”,Ability 是大楼的主入口,pages 目录则是每一层的户型图,resources 就是装修材料库。

理解了这个结构,你就能明白,开发页面这件事,本质上就是在 pages 目录里建 .ets 文件,然后用 ArkTS 语法描述页面长什么样。

2.2 页面文件的骨架:@Entry 与 @Component

打开默认生成的 Index.ets,你会看到类似这样的代码:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height('100%')
  }
}

这里的 @Entry 和 @Component 是理解 ArkUI 页面的钥匙。

@Entry 表示这个组件是一个页面的入口。一个 .ets 文件里只能有一个 @Entry,对应一个页面。就像一栋楼只能有一个正门,你可以有很多个单元入口,但主门只有一个。

@Component 表示这是一个自定义组件。你可以把页面拆成多个组件,每个组件负责一块 UI,然后用 @Component 定义,再用 @Entry 把最外层的根组件标记为页面入口。

struct Index 是组件的定义体,build() 方法则负责描述这个组件长什么样。

这里有个初学者经常搞混的点:build() 里只能有一个根节点。Column、Row 这些容器组件可以往里面塞任意多个子组件,但从 build() 的第一层看,它只能返回一个根组件。这不是限制,而是一种设计约束,能让 UI 结构更清晰,避免出现模棱两可的布局。

3. 页面构建的核心细节与实操要点

3.1 常用基础组件:从 Text 到 TextInput

掌握了页面骨架之后,下一步就是往里面填东西。ArkUI 内置了一批常用组件,我把平时项目里用得最多的给你列出来,并且把一些文档里看不太到的细节也一并说了。

Text 是最基础的文本组件。它有两个构造参数,第一个是文本内容,第二个是样式:

Text('你好,鸿蒙')
  .fontSize(16)
  .fontColor('#333333')
  .fontWeight(FontWeight.Medium)
  .textAlign(TextAlign.Center)

注意,fontSize 的单位默认是 fp,也就是 font pixel,它是会跟随系统字体大小设置变化的。如果你的文本是用于标题这类需要固定大小的场景,可以考虑用变量控制,而不是直接写死,方便后续做字体适配。

Button 按钮在构建页面时几乎必用。ArkUI 的 Button 支持多种样式,最常见的用法是:

Button('点击我')
  .onClick(() => {
    // 处理点击事件
  })

如果你需要按钮内部再放点别的元素,比如图片加文字的复合按钮,可以使用 Button 的另一种构造函数,传入一个自定义组件:

Button() {
  Row() {
    Image($r('app.media.icon')).width(20).height(20)
    Text('下一步').fontSize(16)
  }
}

TextInput 是输入框组件,常用于登录页、搜索框:

TextInput({ placeholder: '请输入用户名' })
  .type(InputType.Normal)
  .maxLength(20)
  .onChange((value: string) => {
    this.username = value;
  })

实战中我建议你给输入框设置 maxLength,否则用户一口气输入几百个字,界面会变得很尴尬,数据校验也会更麻烦。另外,type 属性记得按需设置,比如密码框用 InputType.Password,手机会自动把输入内容打码显示。

Image 组件用来展示图片:

Image($r('app.media.logo'))
  .width(120)
  .height(120)
  .borderRadius(60)

这里的 $r 是资源引用的语法,图片需要放到 resources/base/media 目录下。用资源引用的好处是,系统会根据屏幕密度自动选择合适分辨率的图片,同时也方便后续做多语言、多主题的适配。

3.2 容器组件与布局实操:Column、Row、Stack、Scroll

有了一批基础组件,下一步就是如何组织它们。ArkUI 的容器组件提供了不同布局能力,我挑最关键的几个说。

Column 是纵向布局容器,里面子组件从上往下排列。Row 相反,从左往右排。这两个是使用频率最高的布局容器。

Column() {
  Text('标题').fontSize(20)
  Text('副标题').fontSize(14).fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)

默认情况下,Column 里的子组件是水平居中的,如果要改为左对齐,用 alignItems(HorizontalAlign.Start)。Row 里修改垂直对齐方式用 alignItems(VerticalAlign.Top) 这类方法。

Stack 是层叠布局,后放的子组件会盖在先放的上面。非常适合做“中间一个内容 + 右上角关闭按钮”这类浮层效果:

Stack() {
  // 底层内容
  Column() {
    Text('弹窗内容')
  }
  .width(300).height(200).backgroundColor('#FFFFFF').borderRadius(16)
  
  // 右上角关闭按钮
  Button('X')
    .position({ x: 280, y: -10 })
}

Scroll 组件解决的是内容超出屏幕的问题。HarmonyOS 里默认情况下页面内容超出屏幕是不会滚动的,你必须在内容层外面套一个 Scroll:

Scroll() {
  Column() {
    // 这里放你的长内容
  }
  .width('100%')
}
.scrollBar(BarState.Auto)

有一个容易踩的坑:Scroll 里套的 Column,一定要设置 .width('100%'),否则内容区域宽度会自适应子组件宽度,看起来就会内容没有占满全屏,背景色只出现在内容那一块,看起来非常怪。

3.3 页面状态管理与数据驱动刷新

前面说了声明式 UI 的核心是“数据变了,界面自动更新”,这里状态管理的具体实现同样基于这个理念。

在 ArkUI 里,最常用的状态装饰器有这些:

  • @State:组件内部使用的状态,变量变化时,依赖它的 UI 会自动刷新
  • @Prop:子组件接收父组件传过来的值,单向同步
  • @Link:子组件和父组件的值双向同步
  • @Provide / @Consume:跨层级共享状态,不用一层层传

用一个计数器实例来看 @State 的实际效果:

@Entry
@Component
struct CounterPage {
  @State count: number = 0;

  build() {
    Column() {
      Text(`当前计数:${this.count}`)
        .fontSize(24)
        .margin({ bottom: 20 })
      
      Button('点击加一')
        .onClick(() => {
          this.count++;
        })
    }
  }
}

当你点击按钮,this.count 发生变化,Text 组件会立刻更新显示结果。这个过程中你没有写任何一行操作 UI 的代码,UI 是自己“感知”到数据变化的。

很多新手会问,为什么 @State 只能装饰基本类型和简单对象?其实不是只能装饰这些,ArkUI 的 @State 支持多种数据类型,但对于嵌套比较深的对象,普通的赋值可能不会触发 UI 刷新。遇到这种情况,你需要重新给整个对象赋一个新值,或者使用 @Observed 和 @ObjectLink 这套专门处理嵌套对象状态更新的机制。

我建议你在项目初期就定好状态管理的规范:页面内部的临时 UI 状态用 @State,父子组件传参用 @Prop 和 @Link,全局共享数据(比如用户信息)则考虑引入 AppStorage 或者像 VModel、RxJS 这类库统一管理。不然到项目后期,状态满天飞,改一个数据引出一堆问题,排查起来会相当痛苦。

3.4 页面路由与多页面跳转

一个正规的应用不可能只有一个页面。从 A 页面跳 B 页面,再把参数带过去再带回来,这就要用到路由了。

HarmonyOS 的页面路由有两种主流方案:router 和 Navigation。

router 是最直白的跳转方式:

// 引入 router
import { router } from '@kit.ArkUI';

// 跳转到新页面
router.pushUrl({
  url: 'pages/second/DetailPage',
  params: {
    id: 123,
    name: '张三'
  }
});

在目标页面里接收参数:

import { router } from '@kit.ArkUI';

@Entry
@Component
struct DetailPage {
  @State id: number = 0;
  @State name: string = '';

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, Object>;
    if (params) {
      this.id = params.id as number;
      this.name = params.name as string;
    }
  }
}

这个方法简单直接,我自己在很多项目里也是这么干的。不过,如果你的应用页面层级较深、有很多 tab 页和跳转关系网,那更推荐使用 Navigation。Navigation 是系统级的导航容器,支持路由栈管理、标题栏自动生成、转场动画配置,还能和系统返回手势联动,体验更统一。

用 Navigation 的写法大致是:

@Entry
@Component
struct MyHomePage {
  navDestination: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.navDestination) {
      Button('跳转详情')
        .onClick(() => {
          this.navDestination.pushPathByName('DetailPage', { id: 456 });
        })
    }
  }
}

然后在目标页通过 NavDestination 呈现,需要配置路由表或者在代码里注册。

对于新项目,我的个人建议是:如果页面少于 5 个,直接用 router,简单省事;如果应用较复杂,从一开始就用 Navigation,不然后期从 router 迁移到 Navigation 成本较高,而且要改动每个页面。别问我是怎么知道的,这都是踩过坑换来的经验。

3.5 生命周期与页面刷新时机

页面不是简单地“出现”和“消失”,它还有一整套生命周期回调。搞懂了生命周期,你就能在正确的时机做正确的事。

ArkUI 页面组件常见的生命周期如下:

  • aboutToAppear:组件即将出现,适合做数据初始化
  • onPageShow:页面显示时触发,注意它和 aboutToAppear 的区别在于,onPageShow 在从其他页面返回时也会触发
  • aboutToDisappear:组件即将销毁,适合释放资源、解绑事件
  • onPageHide:页面隐藏时触发,通常是跳转到其他页面时

有一段逻辑可能在项目里会频繁碰到这样的问题:从列表页 A 跳到详情页 B,用户改了一些信息后返回 A,A 页面的数据需要刷新。如果你把刷新逻辑写在 aboutToAppear 里,会发现返回时数据并没有更新。原因就是 aboutToAppear 只在组件第一次创建时调用一次,后续从其他页面返回时,并不会再次触发。

正确的做法,是把刷新逻辑放在 onPageShow 里:

onPageShow() {
  this.loadData(); // 每次回到这个页面时刷新数据
}

注意,onPageShow 只在标记为 @Entry 的页面组件中生效。如果你的自定义子组件里写了 onPageShow,它不会被调用。这也是很多人调试生命周期时发现“怎么没走”的原因之一,子组件里没有这个回调,要用父子组件的状态同步来模拟这个时机。

4. 实操过程:从零搭建一个多页面应用的完整流程

4.1 场景定义与页面规划

理论讲了不少,下面咱们用一个实际项目串一遍。这个项目是一个简单的“待办事项”应用,核心功能有两个:查看待办列表、新增待办事项。

规划下来需要两个页面:

  • 列表页(Index.ets):展示待办事项清单,点击加号按钮跳转到新增页
  • 新增页(AddPage.ets):有一个输入框和一个保存按钮,填写内容后保存,返回列表页并刷新列表

这个场景虽然简单,但它把组件、布局、状态管理、路由、生命周期全覆盖了,非常适合作为页面构建的完整练习。

4.2 列表页实现:组件组合与状态管理

先写列表页。新建一个项目之后,默认的 Index.ets 就是我们的列表页。

@Entry
@Component
struct Index {
  @State todoList: string[] = [];

  aboutToAppear(): void {
    // 模拟本地数据加载
    this.todoList = ['学习 HarmonyOS 页面构建', '写一篇技术博客', '整理项目文档'];
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('待办事项')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        Blank()
        Button('新增')
          .onClick(() => {
            this.toAddPage();
          })
      }
      .width('100%')
      .padding(16)

      // 列表区域
      if (this.todoList.length > 0) {
        List() {
          ForEach(this.todoList, (item: string, index: number) => {
            ListItem() {
              Row() {
                Text(`${index + 1}. ${item}`)
                  .fontSize(16)
                Blank()
                Button('删除')
                  .fontSize(12)
                  .onClick(() => {
                    this.deleteItem(index);
                  })
              }
              .width('100%')
              .padding(12)
              .margin({ bottom: 8 })
              .backgroundColor('#F5F5F5')
              .borderRadius(8)
            }
          }, (item: string, index: number) => `${item}_${index}`)
        }
        .layoutWeight(1)
        .width('100%')
      } else {
        // 空数据占位
        Column() {
          Text('暂无待办事项')
            .fontColor('#999999')
            .fontSize(16)
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
        .width('100%')
      }
    }
    .width('100%')
    .height('100%')
  }

  toAddPage(): void {
    // 跳转到新增页
  }

  deleteItem(index: number): void {
    this.todoList.splice(index, 1);
  }
}

这个文件里有几个值得注意的实操点。

列表渲染用的是 List + ForEach。List 是懒加载列表,适合数据量大的场景,不会一次性把全部内容都创建出来。ForEach 需要提供三个参数:数据源数组、UI 生成函数、key 生成函数。key 函数很关键,如果数组里的数据没有唯一的 id,务必用“数据 + 索引”拼接为 key,这样能帮助系统准确识别哪一项被增删了,避免渲染错乱。

删除操作我用了 splice,这一步会修改 todoList 数组。在 ArkUI 里,@State 变量监听的是状态引用的变化,直接对数组调 splice、push 这类方法也能被感知,但如果替换整个数组,也建议用 splice 或重新赋值的方式,不要用 this.todoList[index] = xxx 这种方式修改元素,这通常不会触发刷新。

空数据占位用了一个 if 条件渲染。这是 ArkUI 支持的条件渲染语法,和 JS 里的 if 类似,当 todoList 为空时显示“暂无待办事项”,有数据时显示列表。这种写法很常见,可以减少页面层级,比用 visibility 控制显隐性能更好。

4.3 新增页实现:参数接收与页面返回

在 pages 目录下新建一个 AddPage.ets 文件,代码如下:

import { router } from '@kit.ArkUI';

@Entry
@Component
struct AddPage {
  @State title: string = '';

  build() {
    Column() {
      Text('新增待办')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 32, bottom: 24 })

      TextInput({ placeholder: '请输入待办事项' })
        .width('90%')
        .height(48)
        .borderRadius(8)
        .backgroundColor('#F0F0F0')
        .padding({ left: 16, right: 16 })
        .onChange((value: string) => {
          this.title = value;
        })

      Button(this.title.trim() === '' ? '请输入内容' : '保存')
        .width('90%')
        .height(44)
        .margin({ top: 24 })
        .enabled(this.title.trim() !== '')
        .onClick(() => {
          if (this.title.trim() === '') {
            return;
          }
          // 返回并带数据给上一页
          router.back({
            uri: 'pages/Index',
            params: {
              newTodo: this.title.trim()
            }
          });
        })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

这里做了一个细节优化:按钮的 enabled 状态和标题显示内容都基于输入框的当前内容实时计算。输入为空时按钮不可点,防止提交空数据。

router.back 的 params 参数用于向下一个页面传递数据,但要注意,这个数据并不是直接给 Index 文件的,而是通过路由参数对象返回。在 Index 的 onPageShow 里,我们需要处理这个参数:

onPageShow(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params.newTodo) {
    this.todoList.push(params.newTodo as string);
  }
}

这是整条链路里最容易忽略的一环。很多人把参数传回来了,但忘记在接收方拿数据,结果发现列表没有刷新。在页面返回时,系统不会重新触发 aboutToAppear,但会触发 onPageShow,所以我们在 onPageShow 里拿路由参数,把它 push 进 todoList,状态变化会自动触发列表重新渲染。

4.4 样式细节打磨与体验优化

页面功能跑通之后,下一步是打磨视觉和交互。不需要做成多华丽,但基本的手感和一致性要有。

布局上下间距尽量使用统一的间距体系。ArkUI 里 margin 和 padding 的单位默认是 vp(virtual pixel),和 dp 类似,会自动适配不同像素密度的屏幕。建议把常用间距定义在 resources 的 float 里,比如 spacing_sm 8、spacing_md 16、spacing_lg 24,引用时用 $r('app.float.spacing_md') 获取,这样全局调整起来只需要改一处。

按钮的点击反馈也很重要。Button 默认有状态变换效果,但自定义的 Row、Column 如果加了点击事件,建议用 .stateEffect(true) 开启点击反馈,这样用户点击时会有轻微的缩放或变色效果,交互感会好很多。

页面的背景色建议统一在根容器上设置。HarmonyOS 默认背景是白色,如果你的页面背景是灰色,只设置了局部的 backgroundColor,内容区域外露出的边边角角会很不协调。把背景色总在最外层 Column 或 Row 上设置即可。

4.5 真机调试与预览器对比

代码写完之后,上线之前的最后一步是调试。DevEco Studio 提供了 Previewer 预览器和模拟器,但我的建议是,有条件尽量用真机调试。

Previewer 适合快速验证组件布局,但它不是真实渲染,有一些系统能力(比如路由跳转、传感器、相机)在预览器里不可用,而且不同设备尺寸下的表现也会有差异。真机调试只需要打开开发者模式,用 USB 连接电脑,DevEco Studio 会自动识别设备,点击运行就能安装到手机上。

调试时有两类问题比较常见。一类是布局问题,页面元素超出屏幕了、被遮挡了、或者在小屏设备上挤得不成样子,这些用 Previewer 很难完全模拟出来。另一类是状态和生命周期相关的问题,比如数据传参、页面刷新时机,这类问题必须实际跑起来才能观察到。

如果你测试时发现界面闪退,先看 Log 面板的报错信息。ArkTS 编译期类型检查比较严格,页面构建里最常见的报错就是组件属性传了错误类型,或者状态变量类型不匹配。这些错误在编译阶段就能发现,关键是别忽略 IDE 的编译警告,插件有时候会“黄标”提示,很多人在编译不报红的情况下就忽略了,结果运行时才崩。

5. 常见问题与排查技巧实录

5.1 页面空白但没报错,是什么原因

有时候运行应用,页面一片空白,控制台也没有错误信息。这种情况让人很头疼,因为你压根不知道从哪下手。

根据我的排查经验,最常见的原因是 build() 里写了多个根组件。ArkUI 要求 build() 里只能有一个根节点,如果你不小心写了两个并列的 Column,编译可能不会报错,但运行时会白屏。检查方法很简单:看 build() 的部分,把并列的一层节点外面再包一个 Column 或 Stack。

还有一种可能是背景色和页面背景融为一体。如果你在最外层设置了和系统背景一样的深色,同时 Text 的颜色也是深色,看起来就像什么都没渲染。这时候可以试着把根组件的 backgroundColor 临时改成红色,一眼就能看出组件到底有没有被渲染出来。

5.2 @State 变量修改了但 UI 没刷新

这是状态管理里最经典的坑。你写了 this.obj.name = 'xxx',结果界面上没有变化。原因在前面提到过一部分,@State 对嵌套对象内部属性的修改感知能力有限。

解决方案有两个。一是重新赋一个全新的对象:

this.obj = { ...this.obj, name: 'xxx' };

二是使用 @Observed 装饰类,配合 @ObjectLink 装饰变量,实现深度观测:

@Observed
class TodoItem {
  title: string;
  done: boolean;
  constructor(title: string) {
    this.title = title;
    this.done = false;
  }
}

然后在组件里用 @ObjectLink 接收:

@Component
struct TodoItemView {
  @ObjectLink item: TodoItem;
  // ...
}

需要提醒的是,@ObjectLink 只能用于装饰类实例,它要求这个类必须被 @Observed 修饰。如果你发现自己碰到了一个数据对象“独立于”任何组件实例的情况,要检查是不是在子组件里忘了加 @ObjectLink,导致传递的引用断开了。

5.3 列表滑动卡顿怎么办

列表页数据量稍微大一点,滑动就开始掉帧,这通常不是 HarmonyOS 本身的问题,而是代码写法有待优化。

优先把 List 组件作为列表容器,而不是在 Column 里用 ForEach 循环生成一大堆布局。List 有懒加载机制,只渲染当前可视区域的列表项,Column + ForEach 则会一次性创建所有子组件,数据量一上来,性能差距非常明显。

另外一个常见的性能杀手是每行列表项里都放了复杂的图片加载逻辑或过多的嵌套容器。自定义列表项组件时,尽量让每个 ListItem 内层级简单一些,图片用官方 Image 组件并配合懒加载占位,避免在滚动过程中频繁做资源解码和重新布局。

还有一些细节是 setState 刷新的粒度。如果你在列表页面里频繁修改 @State 数组,每次修改都会触发整个列表的 diff 更新。尽量合并数据的修改批次,比如一次向数组里 push 多条数据,而不是一条一条地改。

5.4 路由跳转时参数类型不匹配

router 传参时,params 是一个对象,接收时统一被当作 Object 类型。很多人在接收端直接做类型断言,比如 params.id as number ,如果传的参数值实际是字符串或者没传,运行时会出现 undefined 或类型错误。

稳妥的做法是接收端做一次默认值保护:

const rawParams = router.getParams() as Record<string, Object>;
const id = rawParams?.id !== undefined ? Number(rawParams.id) : 0;

这样即使页面被异常打开、参数没传到位,也不会直接崩溃,最多显示默认值。尤其是在使用统一导航、或者从通知栏点击消息直达某个页面的场景,参数缺失的情况非常常见,这套保护逻辑值得成为你写页面的默认习惯。

5.5 常见坑位速查表

症状 可能原因 解决方案
页面白屏无报错 build() 里多个根节点 用一个容器组件包住所有内容
@State 对象修改不刷新 嵌套对象深度检测有限 整体重新赋值,或使用 @Observed/@ObjectLink
返回上一页数据不更新 aboutToAppear 不会重复调用 把刷新逻辑放到 onPageShow 中
列表卡顿 Column + ForEach 全量渲染 改用 List + ForEach 懒加载
子组件生命周期回调不触发 自定义组件没有 @Entry 标记 页面级生命周期只在 @Entry 页面生效
输入框光标消失 TextInput 宽高设置过小 给输入框设置足够高度,或使用默认高度

这张表是我做项目时整理的真实现场问题,不是从文档里抄出来的。每一条基本都折腾过我,写出来希望能帮你省下半天调试功夫。

6. 页面构建的进阶思路

页面构建学到这个地方,我相信你已经能独立完成一个多页面的应用原型了。最后再送你两个方向,算是给后续进阶指个路。

一个是深入数据持久化。页面状态存内存里,一重启就没了。学会结合 Preferences、关系型数据库或者分布式数据服务,把用户的待办事项持久化下来,你的应用才真的“能用了”。Preferences 适合存 KV 结构的小数据,比如用户偏好设置;待办列表这种结构化数据,建议用关系型数据库(RelationalStore),不掉数据,查询也灵活。

另一个是关注组件复优。一个页面拆成多个自定义组件,数据通过 @Prop、@Link 传递,组件之间尽量解耦。拆组件的好处不只是代码短了,更重要的是每个组件可以单独做性能优化,复杂页面看起来更清晰、更容易排查问题。

我个人实际开发中最深的一个体会是:HarmonyOS 的页面构建并不难,真正决定代码上限的是你对自己页面状态的管理思路。数据理清楚了,页面怎么拼都不会乱;数据是一团浆糊,再炫的组件库也写不出稳定的应用。所以,多花时间在状态设计和数据流梳理上,比硬啃一堆 UI 组件 API 要值得多。

Logo

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

更多推荐