系列教程:
《鸿蒙纪元》 是 张风捷特烈[1] 计划打造的一套 HarmonyOS 开发系列教程合集。致力于创作优质的鸿蒙原生学习资源,帮助开发者进入纯血鸿蒙的开发之中。本系列的所有代码将开源在 HarmonyUnit[2] 项目中:github: https://github.com/toly1994328/HarmonyUnit
gitee: https://gitee.com/toly1994328/HarmonyUnit
鸿蒙纪元 系列文章列表可在《文章总集》[3] 或 【github 项目首页】[4] 查看。本文是《鸿蒙纪·梦始卷》 的第五章,上一篇我们介绍了 猜数字 的基本功能,并完成了基本的界面布局。了解通过拆分文件,将代码逻辑拆解成多个文件模块维护:本文将继续完善猜数字需求,完成交互与状态变化。
在编写代码之前,最好仔细 分析需求 。归纳一下界面中交互时会变化的状态量,包括它的类型、修改的时机、以及状态量变化的逻辑。
这样就可以通过 @State 定义组件中的状态量,如下所示:@Component
export struct GuessingPage {
@State guessing: boolean = false;
@State secret: number = 0;
@State input: string = '';
@State result: CheckResult = CheckResult.none;
点击右下角的按钮开始生成数字,可以定义一个 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 里面(如下反例)。单独封装函数处理,可以让构建逻辑相对精简,也可以独立出修改状态数据的逻辑。可谓一举两得,特别是对了较为复杂的逻辑。输入框视图 在输入过程中可以影响 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,
}
)
我们可以通过闭包调用方法的方式构建组件,这样就能有期望的 this 指向:---->[修改传参方式,通过闭包,以调用的方式,此时 titleInput 中的 this 是 GuessingPage]----
AppBar(
{
/// 略...
titleSlot: () => {this.titleInput()},
}
)
点击顶部栏右侧的运行按钮时,会触发比较逻辑。检验输入值和目标值的大小关系;上一章介绍说过,校验的结果通过 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
}
}
在声名式的 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)
}
}
很多初学者可能没有拆分的意识,喜欢把所有的逻辑一股脑全塞在一块,最后形成一个难以维护的臃肿项目。我建议,大家在敲代码之前,一定要好好分析一下功能需求和界面结构;认清 交互事件 和 状态数据 流向。争取有一个好的代码结构,可以让项目代码非常整洁、易读、清晰。
这里提交一个小里程碑:v8-猜数字-交互完成[5]。
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 中的代码压力,从而专注于界面构建逻辑。
对于更加复杂的业务逻辑,还可以继续根据职责进行拆分。不过目前的猜数字项目这样就已经非常不错了,各个文件各司其职,共同维护猜数字小系统的运行。
这里提交一个小里程碑:v9-猜数字-代码结构优化[6]。大家可以和 V8 对比, 感受一下代码结构带来的效力。
到这里,我们就完成了猜数字的基本功能。下一篇,我们将了解一下动画的使用,在每次猜测时,结果面板都可以动画表现。
引用链接
[1] 张风捷特烈: https://juejin.cn/user/149189281194766
[2] HarmonyUnit:https://github.com/toly1994328/HarmonyUnit
[3] 《文章总集》:https://juejin.cn/column/7392249184122060836
[4] 【github 项目首页】: https://github.com/toly1994328/HarmonyUnit
[5] v8-猜数字-交互完成: https://github.com/toly1994328/HarmonyUnit/tree/42aca099038678360434ba892d370b60b6baf98c
[6] v9-猜数字-代码结构优化: https://github.com/toly1994328/HarmonyUnit/tree/dc33ec1b19c11208f9bbcf8fcf3a3b372765de6a
[7] 张风捷特烈: https://juejin.cn/user/149189281194766
[8] 张风捷特烈: https://space.bilibili.com/390457600
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!