鸿蒙纪·梦始卷#05 | 猜数字 - 交互与状态变化

鸿蒙纪·梦始卷#05 | 猜数字 - 交互与状态变化

技术博客 admin 128 浏览

《鸿蒙纪元》张风捷特烈 计划打造的一套 HarmonyOS 开发系列教程合集。致力于创作优质的鸿蒙原生学习资源,帮助开发者进入纯血鸿蒙的开发之中。本系列的所有代码将开源在 HarmonyUnit 项目中:

github: github.com/toly1994328…
gitee: gitee.com/toly1994328…

鸿蒙纪元 系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


本文是《鸿蒙纪·梦始卷》 的第五章,上一篇我们介绍了 猜数字 的基本功能,并完成了基本的界面布局。了解通过拆分文件,将代码逻辑拆解成多个文件模块维护:

本文将继续完善猜数字需求,完成交互与状态变化。


1. 状态数据分析

在编写代码之前,最好仔细 分析需求 。归纳一下界面中交互时会变化的状态量,包括它的类型、修改的时机、以及状态量变化的逻辑。

启动 点击开始 输入猜测

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

状态量 类型 含义 变化时机
secret number 生成的目标数字 点击生成按钮时
input string 输入的字符串 输入事件
guessing bool 是否猜测中 生成数字后,猜对之后
result CheckResult 猜测结果枚举 点击校验时

这样就可以通过 @State 定义组件中的状态量,如下所示:

ts
代码解读
复制代码
@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 清空,表示新的一局开始;
ts
代码解读
复制代码
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. 进行实现,如下所示:

ts
代码解读
复制代码
@Builder titleInput() { TextInput({ placeholder: '输入 0~99 数字', text: $$this.input, }) /// 略同... }

坑点: 封装时插槽组件构建的 this 指向

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

ts
代码解读
复制代码
---->[之前传参方式, titleInput 中thisAppBar]---- AppBar( { /// 略... titleSlot: this.titleInput, } )

我们可以通过闭包调用方法的方式构建组件,这样就能有期望的 this 指向:

ts
代码解读
复制代码
---->[修改传参方式,通过闭包,以调用的方式,此时 titleInput 中的 thisGuessingPage]---- AppBar( { /// 略... titleSlot: () => {this.titleInput()}, } )

4. 核心校验逻辑

点击顶部栏右侧的运行按钮时,会触发比较逻辑。检验输入值和目标值的大小关系;上一章介绍说过,校验的结果通过 CheckResult 枚举表示:

ts
代码解读
复制代码
enum CheckResult { none, bigger, smaller, equal, }

校验的逻辑封装为 checkResult 方法,其中会处理状态数据的变化,如下所示:
仅当输入非空、游戏开始后才需要进行校验,如果输入不是数字则不处理。然后计算输入值和目标值的差值,更新 this.result 即可:

ts
代码解读
复制代码
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 框架中,都是基于数据来决定界面的构建。状态数据界面表现的决定因素,比如中间的描述信息,在不同状态数据下有不同的界面表现:

开始: guessing=false 生成随机数后 guessing=true 猜对时 result = CheckResult.equal

声名式 UI 的另一大特点是:

界面构建的逻辑可以被 分离 ,局部界面只需要依赖它所数据。

比如中间的介绍信息,需要依赖 resultguessingresult 三个状态数据;我们可以将其封装为 InfomationDisplay 组件,来单独维护中间区域的界面构建逻辑。在主界面构建时,只需要使用该组件,传入数据即可:

这样 InfomationDisplay 中就可以专注于处理,中间内容根据状态数据展示不同的文字。如下所示, infovalue 两个函数用于处理展示的字符串。这就是职责的分离,每件事都有专门负责的人,出了问题或需要更新需求时,就可以迅速找到负责这件事的类、函数。
在子组件中,可以通过 @Prop 声明 父子单向同步 的参数,这样父层级传入的数据变化时,可以自动通知更新当前组件:

ts
代码解读
复制代码
@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) } }

很多初学者可能没有拆分的意识,喜欢把所有的逻辑一股脑全塞在一块,最后形成一个难以维护的臃肿项目。我建议,大家在敲代码之前,一定要好好分析一下功能需求,和界面结构;认清 交互事件状态数据 流向。争取有一个好的代码结构,可以让项目代码非常整洁、易读、清晰。
这里提交一个小里程碑:v8-猜数字-交互完成


6. 文件拆分维护

GuessingPage.ets 中的代码目前有 200 多行,看起来代码还是比较清晰的。最后,我们运用一些上一章文件拆分的思想,对它进行拆解,分多个文件共同维护,进一步提高代码的可读性:
如下所示,将 GuessingPage.ets 的代码按照类型和功能进行整理,放入 page/guessing 文件夹下;实现其中主要包括 状态数据的维护界面构建逻辑 ,分别将它们放入 modelview 文件夹下。这样一眼就能看到,那个文件在负责哪件3事:

此时 状态数据数据变化逻辑 集中在 GuessingState 中,我们后续将称修改数据的逻辑为 业务逻辑。 此时就实现了最简单的 业务逻辑视图构建逻辑 的分离:

dart
代码解读
复制代码
---->[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 中的代码压力,从而专注于界面构建逻辑。

对于更加复杂的业务逻辑,还可以继续根据职责进行拆分。不过目前的猜数字项目这样就已经非常不错了,各个文件各司其职,共同维护猜数字小系统的运行。
这里提交一个小里程碑:v9-猜数字-代码结构优化。大家可以和 V8 对比, 感受一下代码结构带来的效力。


尾声

到这里,我们就完成了猜数字的基本功能。下一篇,我们将了解一下动画的使用,在每次猜测时,结果面板都可以动画表现。
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。关注 公众号 并回复 鸿蒙纪元 可领取最新的 xmind 脑图电子版,让我们一起成长,变得更强。我们下次再见~

源文:鸿蒙纪·梦始卷#05 | 猜数字 - 交互与状态变化

如有侵权请联系站点删除!

Technical cooperation service hotline, welcome to inquire!