开发体验

第14章 理解应用的结构

本章将从程序员的视角来探查应用的结构问题。先从一个经典的比喻开始,将应用理解为一份菜谱,继而将应用理解为对特定事件进行响应的一系列组件。本章还将探究应用中如何设置问题,如何执行一系列重复的操作,如何记住某些东西,以及如何与网络进行交互。所有这些内容都会在后面几章中详细叙述。

大多数人是从用户的角度来描述一个应用的,但是在程序员看来,应用要复杂得多。我们必须充分理解应用的内部结构,才能更加有效地创建应用。

通常从两个方面来描述应用的内部结构:组件及行为。这大体上与App Inventor的两个主要视图相对应:设计视图及编程视图。前者用来设定应用中的对象(组件),而后者用来编写程序,实现对用户及外部事件的响应(即应用程序的行为)。

在图14-1中,对应用的架构给出了总体描述。本章将对这种架构进行深入细致的探讨。

图14-1 App Inventor应用的内部结构

组件

应用中的组件分为两大类:可视组件及非可视组件。可视组件是在应用启动后能够看到的组件,如按钮、文本输入框及标签等,这些组件构成了应用的用户界面。

非可视组件是不可见的,因此它们不是用户界面的组成部分,通常用于访问设备的内置功能,例如,短信收发器组件用于收发短信,位置传感器组件用于确定设备的位置,而语音合成器组件用于朗读文字。非可视组件是设备的技术核心,是应用程序中的无名英雄。

无论是可视组件,还是非可视组件,本质上都是由一组数据构成的,这组数据被称作属性。属性相当于组件信息的存储器。如可视组件的宽度、高度及对齐等属性,共同定义了组件的外观。

不妨将组件的属性理解为一排表格:在设计视图中设定表格中的值,来定义组件的初始样式。这些样式可以在编程视图中用代码来进行修改。

行为

一般来说,人们很容易理解组件的用途:文本输入框用于输入信息,按钮用来点击等,这些都是很直观的体验。但是应用中的行为往往是抽象的和复杂的。行为定义了应用对事件的响应,无论是用户发起的事件(如点击按钮),还是外部事件(如手机收到短信)。定义这些交互行为的难度恰恰是编程的挑战性所在。

幸运的是,App Inventor提供了一种非常适合于定义行为的可视化“块”语言。与传统的输入代码的编程语言相比,用块语言编程就像玩拼图游戏一样,只是将“代码”碎片按照正确的方式拼装在一起,而无须学习并输入大量的代码。同时,App Inventor采用的事件响应机制,使得对应用行为的定义变得非常简单。下面的几个小节中,我们会举例说明什么是应用的行为,以及如何在App Inventor中设置应用的行为。

应用如食谱

人们习惯于拿软件与食谱做对比。食谱用于指导厨师按照某个特定的流程,对特定的食材进行加工、烹制;传统的软件由一系列的指令构成,而计算机则是按照既定的顺序执行这些指令,如图14-2所示。

图14-2 传统的软件是按顺序执行一系列的指令

一个典型的应用可能开始一项银行交易(A),执行一些计算并修改客户的账户(B),然后在屏幕上显示新的账户余额(C)。

应用是一组事件处理程序

这种食谱模式的软件可以很好地满足早期计算机的应用——执行大量的数值运算。然而,如今计算机的应用已经远远超出数值运算的范围,无论是手机应用、网络应用还是桌面应用,食谱式的代码组织形式已经远远不能满足应用的需求。现在的应用不再是顺序地执行一系列指令,而是对事件做出响应。

事件的触发者通常是最终用户。例如,当用户点击屏幕上的按钮时,会触发按钮的点击事件,程序会对此做出响应,执行某些操作(如发送短信)。同样是手指对屏幕的操作,对于使用触屏的手机或设备来说,当用户的手指在屏幕上划动时,将触发另一类事件——划动事件。在应用中可以利用这类事件在屏幕上绘画,即在手指最初的落点与最终的抬起点之间画一条线。

当今的应用更适合概括为“事件应答机”。这类应用中依然包含了“食谱”——一些顺序执行的指令,但每个食谱仅限于对某些特定事件做出响应,如图14-3所示。

图14-3 拥有多个“事件食谱”的应用

因此,当事件发生时,应用调用执行一系列的指令来实现对事件的响应。这些指令与组件相关,或者说这些响应通过组件体现出来,它们可能是实现组件的某些功能,如发送短信,或者对组件的属性进行修改,比如在用户界面上修改标签的显示文本属性。调用这些指令意味着执行这些指令,让它们产生作用。我们把事件,连同对事件进行响应的一系列指令统称为事件处理程序(Event Handler)。

许多事件由最终用户触发,但有些则不是。应用可以对手机内部的事件进行响应,如方向传感器的变化以及时钟的运转(即时间的流逝),也可以对手机以外的事件做出响应,如来自于其他手机的事件,或收到来自网络的数据,等等。如图14-4所示。

图14-4 应用可以对内部及外部事件进行响应

之所以称App Inventor编程为“直觉”编程,是因为这种编程模式完全基于对事件的响应,而“事件处理程序”则是该语言中最重要的词汇(在其他语言中情况未必如此)。想要定义某个行为,首先要拖出一个事件块。事件块在形式上是这样的:“当...时”。假设有这样一个“朗读”应用:当用户点击按钮时,应用大声读出用户输入的文字。这个应用只需要一个事件处理程序,如图14-5所示。

图14-5 “朗读”应用中的事件处理程序

这些块定义了一组操作:当用户点击朗读按钮时,语音合成器组件将朗读一段文字——用户在文本输入框中输入的内容。在这里,事件是“朗读按钮被点击”,对事件的响应是调用语音合成器的合成语音功能。事件处理程序中包括了图14-5中的所有块。

在App Inventor中,所有动作都发生在对事件的响应之中。应用中不可能存在事件块“当...时”之外的块,如图14-6这样的单摆浮搁的块是毫无意义的。

图14-6 事件处理程序之外散在的块毫无用处

事件类型

表14-2中列出了所有可以引发动作的事件,共有五种类型。

表14-2 能够引发动作的事件

事件类型 举例
用户引发的事件 当用户点击按钮1时,执行...
初始化事件 当应用启动时,执行...
计时器事件 当20毫秒过去时,执行...
动画事件 当两个物体碰上时,执行...
外部事件 当电话收到短信息时,执行...

用户引发的事件

用户引发的事件是一种最常见的事件类型。在输入表单中,最典型的事件就是点击按钮事件,它将引发应用做出某种响应。在图形化界面的应用中,更多的是对触摸及拖拽事件的响应。

初始化事件

有时需要在应用启动时实现某些功能,这时,你无法借助于最终用户引发的事件或其他类型的事件。在这种情况下,事件响应机制是否还能奏效呢?

像App Inventor这样基于事件机制的语言中,应用的启动也被视为一种事件。如果你想在应用打开的同时实现某些功能,可以拖出Screen1的初始化事件块,并将某些功能模块放在其中。

例如,在第3章打地鼠游戏中,在应用启动的同时,通过调用移动地鼠过程,将地鼠放置在一个随机的位置,如图14-7所示。

图14-7 应用启动时,使用Screen1的初始化事件来放置地鼠

计时器事件

应用中的某些活动是由时间的流逝而触发的,比如动画。可以理解为计时器的计时事件触发了角色的移动。App Inventor有一个计时器组件,用于触发计时事件。例如,如果想让一个球在一定时间间隔内,在屏幕上水平向右移动10个像素,就可以使用图14-9中所示的块。

图14-8 计时器一旦开始计时,在每次计时器事件中移动球

动画事件

在App Inventor中,精灵组件是可以在画布组件内自由移动的图形对象。精灵的活动将触发动画事件,即当两个精灵发生碰撞时,或某个精灵到达画布的边界时,都将触发动画事件。在编写游戏或其他交互式动画程序时,通常利用动画事件来定义游戏或动画中的情节。更多信息请参见第17章。

图14-9 当飞碟精灵碰上其他精灵时,播放音效

外部事件

当手机接收到来自GPS卫星的位置信息时,将触发一个外部事件;同样,当手机收到短信时,也会触发此类事件(图14-10)。

图14-10 当手机收到短信时,触发短信收发器组件的收到消息事件

像这类来自设备之外的输入信息都被视为外部事件,它们的作用无异于用户点击按钮事件。

因此,当你在创建一个应用时,从本质上讲,是在创建一系列的事件处理程序:其中之一与应用的初始化有关,其余的事件处理程序针对最终用户的输入,或与时间的流逝有关,或与外部事件有关。你的任务是以事件为线索构思整个应用,然后针对不同的事件设计出不同的响应方式。

在事件处理程序中设置问题

对事件的响应不总是单线条的食谱,程序可以设置问题,也可以重复某些操作。设置问题意味着就应用中的数据进行提问,并根据答案决定下一步程序的走向(分支)。我们将这类有多种可能走向的程序称为“条件分支”结构,如图14-11所示。

图14-11 由问题的答案决定程序走向的事件处理程序

在这种模式下,当事件发生时,程序首先执行指令A,然后对条件进行判断。如果条件成立,则执行功能模块B1,否则,执行功能模块B2。无论执行哪个模块,最终程序都回继续执行指令C。

测试条件可能是类似这样的问题:“分数到达100了吗?”或者“刚才收到的短信是小明发来的吗?”测试条件也可能包含更为复杂的规则,如包含多种关系运算符(小于、大于、等于)以及逻辑运算符(并且、或者、非)的表达式。

在App Inventor中,使用“如果...则”块、“如果...则...否则”块来设定条件行为。例如,在图14-12中,当玩家的分数为100时,程序将显示“你赢了!”。

图14-12 如果分数超过100,则提示玩家获胜

第18章中将详细讨论条件块。

在事件处理程中重复执行指令

程序不仅可以设置问题,并根据答案执行不同分支,还可以重复执行某些操作。App Inventor提供了三种用于重复执行的块:遍历数字循环、遍历列表循环以及满足条件循环。三种循环块中都会包含若干行代码块。顾名思义,遍历列表循环是对列表中的每一项,执行循环块中包含的所有代码块。例如,如果你想给电话号码列表中的每个人都发送同一条短信,可以利用图14-13中的块来实现。

图14-13 遍历列表循环:对电话本列表中的每一项执行同样的操作

遍历列表循环块中的代码块会重复执行多次,这里是3次,因为电话本列表中只有三项。因此“想你!”这样的短信将发往这三个号码。第20章将详细讨论重复块。

事件处理程序可以实现存储功能

在事件处理程序运行过程中,通常需要保存(记住)某些信息。有些信息可以保存在内存中,被称作变量。变量在编程视图中定义。变量与组件的属性相类似,但与任何组件无关。例如,在游戏类应用中,可以定义一个叫作“分数”的变量,当用户执行了某些操作时,事件处理程序会相应地修改分数的值。变量用于在应用运行过程中临时保存数据,一旦退出应用,数据将不复存在。

有时不仅需要在应用运行时保存某些数据,而且要求在退出应用又重新打开应用时,数据依然存在。例如,你想保存一个游戏的历史最高得分,就需要长期保存数据,以便下次有人再玩游戏时,可以看到这个历史记录。在应用关闭后依然保存下来的数据称为永久性数据,这些数据被保存在某种类型的数据库中。

在第15章及第22章,我们将分别讨论临时存储(变量)及永久存储(数据库)数据的问题。

事件处理程序可以与网络对话

有些应用只能使用手机或设备的内部信息,但许多应用可以与网络进行交互,如在应用中显示一个网页,或向网络服务(也称为API)发送数据请求,等等。这类应用被称为“基于网络的应用”。

Twitter是一个很好的例子,由App Inventor创建的应用可以访问Twitter提供的网络服务。你可以编写一个应用,来请求并显示好友们刚刚发布的推文,也可以随时更新自己的Twitter状态。能够与多个网络服务进行对话的应用被称为聚合类应用,我们将在第24章进行探讨。

小结

应用开发者必须以两种视角来观察一个应用,一个是最终用户的视角,另一个则是自内向外的程序员的视角。利用App Inventor来开发应用,首先要设计应用的外观,然后设计应用的行为——一整套的事件处理程序,让应用按照你的意图去运行。首先,通过在事件处理程序中装配某些代码块来实现对事件的响应,这些代码块可能是与组件相关的指令、条件分支、循环操作、网络调用、数据库操作,等等。然后在手机中运行应用,以便对应用进行实时测试。随着你编写的程序越来越多,你头脑中的概念也会越来越清晰,并对程序的内部结构与外观之间的关系也了然于心,这时,你已经成长为一名真正的程序员了。