《鸿蒙纪元》 是 张风捷特烈 计划打造的一套 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 中this 是 AppBar]----
AppBar(
{
/// 略...
titleSlot: this.titleInput,
}
)
我们可以通过闭包调用方法的方式构建组件,这样就能有期望的 this 指向:
ts
代码解读
复制代码
---->[修改传参方式,通过闭包,以调用的方式,此时 titleInput 中的 this 是 GuessingPage]----
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 的另一大特点是:
界面构建的逻辑可以被
分离
,局部界面只需要依赖它所数据。
比如中间的介绍信息,需要依赖 result
、guessing
、result
三个状态数据;我们可以将其封装为 InfomationDisplay
组件,来单独维护中间区域的界面构建逻辑。在主界面构建时,只需要使用该组件,传入数据即可:
这样 InfomationDisplay
中就可以专注于处理,中间内容根据状态数据展示不同的文字。如下所示, info
和 value
两个函数用于处理展示的字符串。这就是职责的分离,每件事都有专门负责的人,出了问题或需要更新需求时,就可以迅速找到负责这件事的类、函数。
在子组件中,可以通过 @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
文件夹下;实现其中主要包括 状态数据的维护 和 界面构建逻辑 ,分别将它们放入 model
和 view
文件夹下。这样一眼就能看到,那个文件在负责哪件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 脑图电子版,让我们一起成长,变得更强。我们下次再见~
如有侵权请联系站点删除!
Technical cooperation service hotline, welcome to inquire!