HarmonyOS 开发应用开发—猜数字 - 交互与状态变化
本文是《鸿蒙纪·梦始卷》 的第五章,上一篇我们介绍了的基本功能,并完成了基本的界面布局。了解通过拆分文件,将代码逻辑拆解成多个文件模块维护:本文将继续完善猜数字需求,完成交互与状态变化。
上一篇我们介绍了 猜数字 的基本功能,并完成了基本的界面布局。了解通过拆分文件,将代码逻辑拆解成多个文件模块维护:
本文将继续完善猜数字需求,完成交互与状态变化。
1. 状态数据分析
在编写代码之前,最好仔细 分析需求 。归纳一下界面中交互时会变化的状态量,包括它的类型、修改的时机、以及状态量变化的逻辑。

猜数字需求中,可以分析出以下需要变化的状态:

这样就可以通过 @State 定义组件中的状态量,如下所示:
@Component
export struct GuessingPage {
@State guessing: boolean = false;
@State secret: number = 0;
@State input: string = '';
@State result: CheckResult = CheckResult.none;
2. 开启猜数字事件
点击右下角的按钮开始生成数字,可以定义一个 start 函数来维护这个事件中状态量的变化逻辑。如下所示:
- 将
guessing置为 true ,表示游戏开始; - 将
secret设置为 (0,100) 间的随机整数; - 将
result置为 none 、input清空,表示新的一局开始; -
start(): void { this.guessing = true; this.secret = Math.floor(Math.random() * 100); this.result = CheckResult.none; this.input ='' }在 Button 的 onClick 事件中,触发
this.start()即可

在编码习惯上,建议事件由独立的函数处理,而 不是 直接写在 onClick 里面(如下反例)。单独封装函数处理,可以让构建逻辑相对精简,也可以独立出修改状态数据的逻辑。可谓一举两得,特别是对了较为复杂的逻辑

3.输入框与数据双向绑定
输入框视图 在输入过程中可以影响 input 状态值;反过来,设置 input 状态值也可以改变 输入框视图 。这就是:


鸿蒙开发中某些组件与状态的双向绑定,可以通过 $$this. 进行实现,如下所示:
@Builder
titleInput() {
TextInput({
placeholder: '输入 0~99 数字',
text: $$this.input,
})
/// 略同...
}

在开发过程中遇到一个非常坑的点,在 GuessingPage 中定义的 titleInput 插槽,在运行时无法访问到类中的状态成员。结果调试发现,直接将 titleInput 作为入参传给 AppBar ,运行该方法的 this 居然是 AppBar。怪不得无法访问 GuessingPage 中的成员呢。

---->[之前传参方式, titleInput 中this 是 AppBar]----
AppBar(
{
/// 略...
titleSlot: this.titleInput,
}
)
![]()

---->[修改传参方式,通过闭包,以调用的方式,此时 titleInput 中的 this 是 GuessingPage]----
AppBar(
{
/// 略...
titleSlot: () => {this.titleInput()},
}
)
4. 核心校验逻辑
点击顶部栏右侧的运行按钮时,会触发比较逻辑。检验输入值和目标值的大小关系;上一章介绍说过,校验的结果通过 CheckResult 枚举表示:
enum CheckResult {
none,
bigger,
smaller,
equal,
}
校验的逻辑封装为 checkResult 方法,其中会处理状态数据的变化,如下所示:
仅当输入非空、游戏开始后才需要进行校验,如果输入不是数字则不处理。然后计算输入值和目标值的差值,更新 this.result 即可:
checkResult(): void {
if (this.input === '' || !this.guessing) {
return;
}
const guess: number = Number(this.input);
if (Number.isNaN(guess)) {
return;
}
const diff = guess - this.secret;
if (diff == 0) {
this.result = CheckResult.equal
this.guessing = false;
this.input =''
}
if (diff > 0) {
this.result = CheckResult.bigger
}
if (diff < 0) {
this.result = CheckResult.smaller
}
}
5. 声名式 UI : 数据决定界面
在声名式的 UI 框架中,都是基于数据来决定界面的构建。状态数据界面表现的决定因素,比如中间的描述信息,在不同状态数据下有不同的界面表现:

声名式 UI 的另一大特点是:
![]()
比如中间的介绍信息,需要依赖 result、guessing、result 三个状态数据;我们可以将其封装为 InfomationDisplay 组件,来单独维护中间区域的界面构建逻辑。在主界面构建时,只需要使用该组件,传入数据即可:
这样 InfomationDisplay 中就可以专注于处理,中间内容根据状态数据展示不同的文字。如下所示, info 和 value 两个函数用于处理展示的字符串。这就是职责的分离,每件事都有专门负责的人,出了问题或需要更新需求时,就可以迅速找到负责这件事的类、函数。
在子组件中,可以通过 @Prop 声明 父子单向同步 的参数,这样父层级传入的数据变化时,可以自动通知更新当前组件:
@Component
struct InfomationDisplay {
@Prop result: CheckResult = CheckResult.none;
@Prop guessing: boolean = false;
@Prop secret: number = 0;
info(): string {
if (this.result == CheckResult.equal) {
return '恭喜你猜对啦~';
}
if (!this.guessing) {
return '点击生成随机数';
}
return '开始输入猜数字吧~';
}
value(): string {
if (this.guessing) {
return '**';
}
return this.secret.toString();
}
build() {
Column() {
Text(this.info())
Text(this.value()).fontSize(46).fontColor('#727272')
}.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
}
}
很多初学者可能没有拆分的意识,喜欢把所有的逻辑一股脑全塞在一块,最后形成一个难以维护的臃肿项目。我建议,大家在敲代码之前,一定要好好分析一下功能需求,和界面结构;认清 交互事件 和 状态数据 流向。争取有一个好的代码结构,可以让项目代码非常整洁、易读、清晰。
6. 文件拆分维护
GuessingPage.ets 中的代码目前有 200 多行,看起来代码还是比较清晰的。最后,我们运用一些上一章文件拆分的思想,对它进行拆解,分多个文件共同维护,进一步提高代码的可读性:
如下所示,将 GuessingPage.ets 的代码按照类型和功能进行整理,放入 page/guessing 文件夹下;实现其中主要包括 状态数据的维护 和 界面构建逻辑 ,分别将它们放入 model 和 view 文件夹下。这样一眼就能看到,那个文件在负责哪件3事:

此时 状态数据 和 数据变化逻辑 集中在 GuessingState 中,我们后续将称修改数据的逻辑为 业务逻辑。 此时就实现了最简单的 业务逻辑 和 视图构建逻辑 的分离:
---->[pages/guessing/model/GuessingState.ets]----
import { CheckResult } from "./CheckResult";
export class GuessingState{
guessing: boolean = false;
secret: number = 0;
input: string = '';
result: CheckResult = CheckResult.none;
checkResult(): void {
if (this.input === '' || !this.guessing) {
return;
}
const guess: number = Number(this.input);
if (Number.isNaN(guess)) {
return;
}
const diff = guess - this.secret;
if (diff == 0) {
this.result = CheckResult.equal
this.guessing = false;
this.input =''
}
if (diff > 0) {
this.result = CheckResult.bigger
}
if (diff < 0) {
this.result = CheckResult.smaller
}
}
start(): void {
this.guessing = true;
this.secret = Math.floor(Math.random() * 100);
this.result = CheckResult.none;
this.input =''
}
}
在视图中,只需要依赖业务逻辑对象即可:

视图的交互行为,触发事件影响数据的逻辑,调用业务逻辑对象中的方法处理即可,这样可以大大减轻 GuessingPage.ets 中的代码压力,从而专注于界面构建逻辑。

对于更加复杂的业务逻辑,还可以继续根据职责进行拆分。不过目前的猜数字项目这样就已经非常不错了,各个文件各司其职,共同维护猜数字小系统的运行。
尾声
到这里,我们就完成了猜数字的基本功能。下一篇,我们将了解一下动画的使用,在每次猜测时,结果面板都可以动画表现。
更多推荐



所有评论(0)