ArkUI声明式UI入门:从XML到声明式的思维转变
前言:为什么鸿蒙要用声明式UI?
做过安卓开发的朋友,谁没被XML布局折磨过?
写一个简单的登录页,你得先在XML里拖十几个控件,给每个控件起个id,然后切到Java/Kotlin文件里一个个findViewById,再设置文本、颜色、点击事件。改个按钮颜色要切到XML,改个点击逻辑又要切回代码,来回折腾半天。
最头疼的还是状态更新。比如用户输入用户名后,登录按钮要从灰色变成可点击状态。你得监听输入框的变化,然后手动调用button.setEnabled(true)。状态一多,代码就乱成一团,很容易出现UI和数据不一致的bug。
这就是命令式UI的痛点:你要告诉电脑"一步步怎么做",从创建控件到更新UI,所有细节都要自己管。
而鸿蒙的ArkUI声明式UI,彻底颠覆了这种开发方式。你只需要告诉电脑"我想要什么样子的UI",剩下的交给框架就行。数据变了,UI自动更新,再也不用手动findViewById和setText了。
这篇文章,我会用最直白的语言和最实用的代码,带你彻底搞懂声明式UI的核心思想,完成从XML到声明式的思维转变。看完你就能写出自己的第一个鸿蒙界面。
一、ArkUI开发框架介绍
ArkUI是华为专门为HarmonyOS NEXT打造的UI开发框架,现在纯血鸿蒙只推荐用基于ArkTS的声明式开发范式。
什么是声明式UI?
一句话总结:数据驱动UI,UI是数据的映射。
当数据发生变化时,框架会自动计算出UI的差异,然后只更新需要变化的部分。你不需要关心怎么更新UI,只需要关心数据怎么变。
我用一个最简单的例子,让你一眼看出命令式和声明式的区别:
需求:点击按钮,让文本从"0"变成"1"
安卓命令式写法:
<!-- activity_main.xml -->
<TextView
android:id="@+id/count_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0" />
<Button
android:id="@+id/increment_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击加1" />
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val countText = findViewById<TextView>(R.id.count_text)
val incrementBtn = findViewById<Button>(R.id.increment_btn)
incrementBtn.setOnClickListener {
count++
countText.text = count.toString() // 手动更新UI
}
}
}
鸿蒙声明式写法:
@Entry
@Component
struct Counter {
@State count: number = 0; // 数据
build() {
Column() {
Text(this.count.toString()) // UI直接绑定数据
.fontSize(30);
Button("点击加1")
.onClick(() => {
this.count++; // 只需要改变数据
});
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center);
}
}
看到区别了吗?
- 安卓:你要先创建控件,然后找到它,然后在点击事件里手动更新它的文本
- 鸿蒙:你只需要定义一个数据count,然后把Text组件和count绑定。当count变化时,Text自动更新
这就是声明式UI的魅力。代码更少,逻辑更清晰,出错的概率也更低。
二、组件的基本结构
在ArkUI里,一切都是组件。页面是组件,按钮是组件,甚至一段文本也是组件。
一个标准的自定义组件,结构非常固定,就这几部分:
// 1. @Component装饰器:告诉编译器,这是一个UI组件
@Component
struct HelloComponent {
// 2. 状态变量:驱动UI变化的数据
// 用@State装饰的变量,只要值变了,UI就会自动刷新
@State message: string = "Hello ArkUI";
// 3. build()方法:组件的渲染函数
// 这个方法必须返回一个UI组件,描述这个组件长什么样子
build() {
// 4. 根布局:每个build()方法只能有一个根组件
Column() {
// 5. 子组件:描述UI的具体内容
Text(this.message)
.fontSize(24) // 6. 属性方法:链式调用设置组件样式
.fontColor(Color.Blue);
Button("改变文本")
.onClick(() => {
// 7. 事件处理:响应用户操作
this.message = "你好,鸿蒙!";
});
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center);
}
}
就这么简单!没有XML文件,没有findViewById,所有东西都在一个文件里,逻辑和UI在一起,维护起来特别方便。
三、常用基础组件
掌握这4个基础组件,你就能搞定80%的界面开发了。我只讲最常用的属性和用法,不常用的就不说了,避免信息过载。
3.1 Text:文本组件
用来显示文字,最基础也最常用的组件。
Text("这是一段普通文本")
.fontSize(18) // 字体大小,单位fp
.fontColor('#333333') // 字体颜色,支持十六进制和Color枚举
.fontWeight(FontWeight.Bold) // 字体粗细
.textAlign(TextAlign.Center) // 文本对齐方式
.maxLines(1) // 最多显示几行
.textOverflow({ overflow: TextOverflow.Ellipsis }) // 超出部分显示省略号
.margin({ top: 10, bottom: 10 }); // 外边距
3.2 Button:按钮组件
用来响应用户点击操作。
// 普通文字按钮
Button("登录")
.width('85%')
.height(50)
.backgroundColor('#007aff')
.fontSize(18)
.borderRadius(8) // 圆角
.onClick(() => {
console.log("点击了登录按钮");
});
// 带图标的按钮
Button($r("app.media.ic_search"), "搜索")
.width(200)
.height(45);
// 文字按钮(没有背景)
Button("注册账号")
.fontColor('#007aff')
.backgroundColor(Color.Transparent);
3.3 Image:图片组件
用来显示本地图片和网络图片。
// 本地资源图片(推荐,性能更好)
// 把图片放到entry/src/main/resources/base/media目录下
Image($r("app.media.logo"))
.width(120)
.height(120)
.objectFit(ImageFit.Contain) // 图片缩放模式,保持比例完整显示
.borderRadius(10);
// 网络图片
Image("https://picsum.photos/200/300")
.width(200)
.height(300)
.alt($r("app.media.placeholder")); // 加载失败时显示的占位图
3.4 TextInput:输入框组件
用来接收用户输入的文本。
// 用户名输入框
TextInput({ placeholder: "请输入手机号/用户名" })
.width('100%')
.height(50)
.padding({ left: 15 })
.backgroundColor('#f5f5f5')
.borderRadius(8)
.onChange((value) => {
// 输入内容变化时触发,value就是输入的文本
console.log("用户输入:" + value);
});
// 密码输入框
TextInput({ placeholder: "请输入密码" })
.type(InputType.Password) // 密码类型,输入内容会变成圆点
.width('100%')
.height(50);
四、布局组件
布局组件用来安排子组件的位置和大小。ArkUI有三个最基础也最常用的布局,复杂界面都是它们嵌套组合出来的。
4.1 Column:垂直布局
子组件从上到下,一个挨一个垂直排列。
Column() {
Text("第一行")
Text("第二行")
Text("第三行")
}
.width('100%')
.height(200)
.backgroundColor('#f5f5f5')
.justifyContent(FlexAlign.Center) // 垂直方向对齐方式
.alignItems(HorizontalAlign.Center) // 水平方向对齐方式
.gap(15); // 子组件之间的间距
4.2 Row:水平布局
子组件从左到右,一个挨一个水平排列。
Row() {
Button("取消")
Button("确定")
}
.width('100%')
.height(60)
.backgroundColor('#f5f5f5')
.justifyContent(FlexAlign.SpaceEvenly) // 水平方向均匀分布
.alignItems(VerticalAlign.Center) // 垂直方向居中对齐
.gap(20);
4.3 Stack:层叠布局
子组件堆叠在一起,后面的组件会盖在前面的组件上面。
Stack() {
// 底层:背景图
Image($r("app.media.bg"))
.width('100%')
.height(200);
// 上层:半透明遮罩
Column() {}
.width('100%')
.height(200)
.backgroundColor('rgba(0,0,0,0.3)');
// 最上层:文字
Text("这是层叠在图片上的文字")
.fontSize(20)
.fontColor(Color.White)
.position({ x: 20, y: 150 }); // 绝对定位
}
.width('100%')
.height(200);
小技巧:写界面的时候,先把整个页面拆成一个个小模块,每个模块用一个Column或Row,然后再往里面填子组件。这样思路会非常清晰。
五、实战:写一个完整的登录页面
现在我们把前面学的所有东西组合起来,写一个可以直接用在项目里的登录页面。这个页面包含:logo、标题、用户名输入框、密码输入框(带显示隐藏功能)、登录按钮、忘记密码和注册链接。
直接复制这段代码到DevEco Studio里,就能运行看到效果。
@Entry
@Component
struct LoginPage {
// 用户名和密码
@State username: string = "";
@State password: string = "";
// 密码是否显示
@State isPasswordVisible: boolean = false;
// 是否正在加载
@State isLoading: boolean = false;
build() {
Column() {
// Logo
Image($r("app.media.logo"))
.width(100)
.height(100)
.margin({ top: 80, bottom: 40 });
// 标题
Text("欢迎登录")
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 60 });
// 用户名输入框
TextInput({ placeholder: "请输入手机号/用户名", text: this.username })
.width('85%')
.height(50)
.padding({ left: 15, right: 15 })
.backgroundColor('#f5f5f5')
.borderRadius(8)
.margin({ bottom: 15 })
.onChange((value) => {
this.username = value;
});
// 密码输入框(带显示隐藏)
Stack({ alignContent: Alignment.End }) {
TextInput({ placeholder: "请输入密码", text: this.password })
.type(this.isPasswordVisible ? InputType.Normal : InputType.Password)
.width('100%')
.height(50)
.padding({ left: 15, right: 50 })
.backgroundColor('#f5f5f5')
.borderRadius(8)
.onChange((value) => {
this.password = value;
});
// 密码显示隐藏按钮
Image(this.isPasswordVisible ? $r("app.media.ic_eye_open") : $r("app.media.ic_eye_close"))
.width(24)
.height(24)
.margin({ right: 15 })
.onClick(() => {
this.isPasswordVisible = !this.isPasswordVisible;
});
}
.width('85%')
.margin({ bottom: 20 });
// 登录按钮
Button(this.isLoading ? "登录中..." : "登录")
.width('85%')
.height(50)
.backgroundColor(this.isLoginButtonEnabled() ? '#007aff' : '#cccccc')
.fontSize(18)
.borderRadius(8)
.enabled(this.isLoginButtonEnabled() && !this.isLoading)
.onClick(() => {
this.handleLogin();
});
// 底部链接
Row() {
Text("忘记密码")
.fontColor('#007aff')
.fontSize(14)
.onClick(() => {
console.log("跳转到忘记密码页面");
});
Text("注册账号")
.fontColor('#007aff')
.fontSize(14)
.onClick(() => {
console.log("跳转到注册页面");
});
}
.width('85%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 30 });
}
.width('100%')
.height('100%')
.backgroundColor('#ffffff');
}
// 判断登录按钮是否可用
isLoginButtonEnabled(): boolean {
return this.username.length > 0 && this.password.length > 0;
}
// 处理登录
handleLogin() {
// 简单的输入验证
if (this.username.length < 3) {
console.log("用户名长度不能少于3位");
return;
}
if (this.password.length < 6) {
console.log("密码长度不能少于6位");
return;
}
// 模拟登录请求
this.isLoading = true;
setTimeout(() => {
this.isLoading = false;
console.log(`登录成功!用户名:${this.username}`);
// 这里可以添加跳转到首页的逻辑
}, 2000);
}
}
这个登录页面有几个实用的细节:
- 密码显示隐藏功能,用户可以点击眼睛图标切换
- 登录按钮只有在用户名和密码都输入后才会变成可点击状态
- 点击登录后会显示加载状态,防止用户重复点击
- 有简单的输入验证
总结
今天这篇文章,核心就是帮你完成一个思维转变:从"我要怎么一步步构建UI",变成"UI应该是什么样子的"。
记住这几个关键点:
- 声明式UI是数据驱动的,数据变了,UI自动更新
- 一切都是组件,每个组件都有固定的结构
- 掌握Text、Button、Image、TextInput四个基础组件
- 掌握Column、Row、Stack三个基础布局,通过嵌套组合实现任何界面
- 多写多练,在实战中体会声明式UI的优势
福利时间!
我已经为大家准备了这个登录页面的完整源码包,里面包含了所有用到的图标资源,还有更完善的错误提示和交互效果。下载后直接导入DevEco Studio就能运行。
获取方式:
关注我的账号,私信回复"登录页面",即可免费获取完整源码!
下一篇文章,我会带大家学习鸿蒙的状态管理,教你如何在多个组件之间传递数据和共享状态。如果这篇文章对你有帮助,别忘了点赞、收藏、转发给你的朋友!有任何问题,欢迎在评论区留言,我会一一解答。
更多推荐

所有评论(0)