ArkUI声明式UI入门:从XML到声明式的思维转变

前言:为什么鸿蒙要用声明式UI?

做过安卓开发的同学一定对XML布局又爱又恨。写界面要先在XML里定义控件,然后回到Java/Kotlin里findViewById,再设置各种属性和监听事件。一个简单的按钮点击,就要写好几处代码,逻辑和UI完全分离,改个样式要来回切换文件。

传统XML布局的痛点:

  • 代码量大,重复劳动多
  • UI和逻辑分离,维护困难
  • 状态更新繁琐,容易出错
  • 无法直接在代码中看到UI结构

而鸿蒙的ArkUI声明式UI彻底解决了这些问题。你只需要描述UI应该是什么样子,框架会自动帮你完成剩下的工作。当状态发生变化时,UI会自动更新,再也不用手动去findViewById和setText了。

这篇文章我会带你彻底理解声明式UI的核心思想,掌握最常用的基础组件和布局,最后用一个完整的登录页面实战,帮你完成从XML到声明式的思维转变。

一、ArkUI开发框架介绍

ArkUI是华为为HarmonyOS NEXT打造的统一UI开发框架,提供了两种开发范式:

  1. 基于JS的Web范式(兼容旧版鸿蒙,纯血鸿蒙已不再推荐)
  2. 基于ArkTS的声明式范式(纯血鸿蒙唯一推荐的开发方式)

声明式UI的核心思想:

你只需要描述"UI在什么状态下应该是什么样子",当状态发生变化时,框架会自动更新UI来匹配这个状态。

简单来说,就是数据驱动UI。数据变了,UI自动跟着变,你不需要关心中间的更新过程。

举个最直观的例子:

  • 命令式(安卓):先创建一个TextView,然后找到它,再调用setText("新内容")
  • 声明式(鸿蒙):Text(this.content),当this.content变化时,文本自动更新

二、组件的基本结构

在ArkUI中,一切都是组件。页面是组件,按钮是组件,甚至一个文本也是组件。一个完整的自定义组件结构如下:

typescript 复制代码
// 1. @Component装饰器:标记这是一个自定义组件
@Component
struct MyComponent {
  // 2. 状态变量:驱动UI变化的数据
  @State count: number = 0;

  // 3. build()方法:组件的渲染函数,必须返回一个UI组件
  build() {
    // 4. 根组件:每个build()方法只能有一个根组件
    Column() {
      // 5. 子组件:描述UI结构
      Text(`点击了${this.count}次`)
        .fontSize(20); // 6. 属性方法:链式调用设置组件样式

      Button("点击我")
        .onClick(() => {
          // 7. 事件处理:响应用户交互
          this.count++;
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center);
  }
}

和安卓XML+Java对比:

安卓写法 鸿蒙声明式写法
XML定义布局 build()方法中直接写UI
findViewById 不需要,直接引用变量
setText() 直接绑定变量,自动更新
setOnClickListener 链式调用.onClick()
布局文件+Java文件 一个文件搞定所有

三、常用基础组件

基础组件是构建UI的最小单元,掌握这四个组件,就能完成80%的界面开发。

3.1 Text:文本组件

用于显示文字内容,是最常用的组件。

typescript 复制代码
Text("这是一段文本")
  .fontSize(20) // 字体大小
  .fontColor(Color.Red) // 字体颜色
  .fontWeight(FontWeight.Bold) // 字体粗细
  .textAlign(TextAlign.Center) // 文本对齐方式
  .maxLines(1) // 最大行数
  .textOverflow({ overflow: TextOverflow.Ellipsis }) // 超出部分显示省略号
  .margin(10); // 外边距

3.2 Button:按钮组件

用于响应用户点击事件。

typescript 复制代码
// 普通按钮
Button("普通按钮")
  .width(200)
  .height(50)
  .backgroundColor('#007aff')
  .onClick(() => {
    console.log("按钮被点击了");
  });

// 带图标的按钮
Button($r("app.media.icon"), "带图标按钮")
  .width(200)
  .height(50);

// 圆形按钮
Button("圆形按钮")
  .width(100)
  .height(100)
  .borderRadius(50);

3.3 Image:图片组件

用于显示本地图片和网络图片。

typescript 复制代码
// 本地资源图片(推荐)
Image($r("app.media.logo"))
  .width(100)
  .height(100)
  .objectFit(ImageFit.Contain) // 图片缩放模式
  .borderRadius(10);

// 网络图片
Image("https://example.com/image.jpg")
  .width(200)
  .height(150)
  .alt($r("app.media.placeholder")); // 加载失败时显示的占位图

3.4 TextInput:输入框组件

用于接收用户输入的文本。

typescript 复制代码
// 用户名输入框
TextInput({ placeholder: "请输入用户名" })
  .width('100%')
  .height(50)
  .margin({ bottom: 15 })
  .onChange((value) => {
    console.log("输入的内容:" + value);
  });

// 密码输入框
TextInput({ placeholder: "请输入密码" })
  .type(InputType.Password) // 密码类型,输入内容会被隐藏
  .width('100%')
  .height(50);

四、布局组件

布局组件用来安排子组件的位置和大小,ArkUI提供了三种最基础也最常用的布局组件。

4.1 Column:垂直布局

子组件从上到下垂直排列。

typescript 复制代码
Column() {
  Text("第一行")
  Text("第二行")
  Text("第三行")
}
.width('100%')
.height(200)
.backgroundColor('#f5f5f5')
.justifyContent(FlexAlign.Center) // 主轴(垂直方向)对齐方式
.alignItems(HorizontalAlign.Center) // 交叉轴(水平方向)对齐方式
.gap(10); // 子组件之间的间距

4.2 Row:水平布局

子组件从左到右水平排列。

typescript 复制代码
Row() {
  Button("按钮1")
  Button("按钮2")
  Button("按钮3")
}
.width('100%')
.height(100)
.backgroundColor('#f5f5f5')
.justifyContent(FlexAlign.SpaceAround) // 主轴(水平方向)对齐方式
.alignItems(VerticalAlign.Center) // 交叉轴(垂直方向)对齐方式
.gap(10);

4.3 Stack:层叠布局

子组件堆叠在一起,后面的组件会覆盖前面的组件。

typescript 复制代码
Stack() {
  // 底层:背景图
  Image($r("app.media.background"))
    .width('100%')
    .height(200);

  // 上层:文字
  Text("层叠在图片上的文字")
    .fontSize(24)
    .fontColor(Color.White)
    .position({ x: 20, y: 20 }); // 绝对定位
}
.width('100%')
.height(200);

布局组合技巧: 复杂的界面都是通过这三种布局的嵌套组合实现的。比如一个列表项可以用Row布局,左边放图片,右边放Column布局,里面放标题和描述。

五、实战:写一个完整的登录页面

现在我们把前面学的所有组件和布局组合起来,写一个标准的登录页面。这个页面包含:logo、标题、用户名输入框、密码输入框、登录按钮、忘记密码和注册链接。

typescript 复制代码
@Entry
@Component
struct LoginPage {
  // 用户名和密码状态
  @State username: string = "";
  @State password: string = "";

  build() {
    Column() {
      // 1. Logo区域
      Image($r("app.media.logo"))
        .width(100)
        .height(100)
        .margin({ top: 80, bottom: 40 });

      // 2. 标题
      Text("欢迎登录")
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 60 });

      // 3. 输入框区域
      TextInput({ placeholder: "请输入手机号/用户名" })
        .width('85%')
        .height(50)
        .margin({ bottom: 15 })
        .padding({ left: 15 })
        .backgroundColor('#f5f5f5')
        .borderRadius(8)
        .onChange((value) => {
          this.username = value;
        });

      TextInput({ placeholder: "请输入密码" })
        .type(InputType.Password)
        .width('85%')
        .height(50)
        .margin({ bottom: 20 })
        .padding({ left: 15 })
        .backgroundColor('#f5f5f5')
        .borderRadius(8)
        .onChange((value) => {
          this.password = value;
        });

      // 4. 登录按钮
      Button("登录")
        .width('85%')
        .height(50)
        .backgroundColor('#007aff')
        .fontSize(18)
        .borderRadius(8)
        .onClick(() => {
          this.handleLogin();
        });

      // 5. 底部链接
      Row() {
        Text("忘记密码")
          .fontColor('#007aff')
          .onClick(() => {
            console.log("点击了忘记密码");
          });

        Text("注册账号")
          .fontColor('#007aff')
          .onClick(() => {
            console.log("点击了注册账号");
          });
      }
      .width('85%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ top: 20 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ffffff');
  }

  // 登录处理函数
  handleLogin() {
    if (this.username === "" || this.password === "") {
      console.log("用户名或密码不能为空");
      return;
    }

    console.log(`登录中... 用户名:${this.username},密码:${this.password}`);
    // 这里可以添加实际的登录逻辑
  }
}

代码说明:

  1. 整个页面使用Column作为根布局,所有元素垂直排列
  2. 使用@State装饰器定义username和password状态变量,输入框的内容会自动同步到这两个变量
  3. 输入框使用TextInput组件,设置了placeholder、背景色和圆角
  4. 登录按钮使用Button组件,点击时调用handleLogin方法
  5. 底部的两个链接使用Row布局,左右分布

你可以直接把这段代码复制到DevEco Studio中运行,就能看到一个完整的登录页面了。

总结

今天我们学习了ArkUI声明式UI的核心思想和基础用法。最重要的是完成思维转变:从"我要一步步构建UI"变成"我要描述UI在不同状态下的样子"。

核心要点总结:

  1. 声明式UI是数据驱动的,数据变了,UI自动更新
  2. 一切都是组件,每个组件由@Component、struct、build()方法组成
  3. 掌握Text、Button、Image、TextInput四个基础组件
  4. 掌握Column、Row、Stack三个基础布局,通过嵌套组合实现复杂界面
  5. 多写多练,在实战中体会声明式UI的优势

福利时间!

我已经为大家准备了这篇文章中登录页面的完整源码,包含了更完善的样式和交互效果,还有输入验证、加载状态等功能。

获取方式:

关注我的账号,私信回复"登录页面",即可免费获取完整源码!

下一篇文章我会带大家深入学习鸿蒙的状态管理,教你如何在多个组件之间共享数据和传递状态。如果这篇文章对你有帮助,别忘了点赞、收藏、转发给你的朋友!有任何问题,欢迎在评论区留言。

相关推荐
ZC跨境爬虫13 小时前
跟着 MDN 学CSS day_29:(掌握文本与字体样式的核心艺术)
前端·css·ui·html·tensorflow
UnicornDev1 天前
【Flutter x HarmonyOS 6】设置页面的UI设计
flutter·ui·华为·harmonyos·鸿蒙
ZC跨境爬虫1 天前
跟着 MDN 学CSS day_31:(精通链接样式,从伪类到导航菜单)
前端·javascript·css·ui·交互
G_dou_1 天前
Flutter+OpenHarmony实战:XMB Tracke
flutter·harmonyos·鸿蒙
lzp07911 天前
元数据驱动开发 - 面向对象编程思想的补充(上)
spring boot·后端·ui
●VON1 天前
鸿蒙Flutter实战:分类管理页BottomSheet CRUD
数据库·flutter·华为·harmonyos·鸿蒙
Meteors.1 天前
安卓源码阅读——01.grade设置binding为true时,xml如何进行映射
android·xml
Ulyanov2 天前
用声明式语法重新定义Python桌面UI:QML+PySide6现代开发入门(一)
开发语言·python·算法·ui·系统仿真·雷达电子对抗仿真
程序员buddha2 天前
传统 Spring 框架,XML 配置 Bean 的方式
xml·java·spring