开发体验

第10章 出题与答题

第8章的“总统问答”应用可以被定制成各种主题的测验,但这里说的定制,仅限于由开发者来修改问题和答案。对于家长、教师或其他用户来说,他们无法创建一份自己需要的试题,也无法修改应用中的问题(除非他们也想学习如何使用App Inventor!)。

本章将创建两个应用:“出题”应用与“答题”应用。其中的“出题”应用,让“教师”可以在用户界面中输入试题和答案,这些信息将被存储在网络数据库中,供其他用户访问;“答题”应用让“学生”可以访问“教师”输入的题目并参加考试。通过创建这两个应用,你将学会如何创建一个由用户输入数据的应用,并实现数据在不同用户之间的共享,这又是一个质的飞跃。

图10-1 运行中的“出题”应用

“出题”与“答题”这两个应用协同工作,让“老师”可以为“学生”出题:父母可以在长途旅行中做一些旅行趣闻类的应用,以增加孩子们的乐趣;小学教师可以创建“数学突击”一类的小测验;大学生们可以创建一系列的考题,来帮助他们的学习小组准备期末考试。本章建立在第8章“总统问答”应用的基础上,并假设你已经完成了那一章的学习。如果你还没有完成,请在学习本章之前,先学完第8章。

本章将分别设计这两个应用:针对“老师”的“出题”应用(见图10-1)以及针对“学生”的“答题”应用,看起来和“总统问答”应用差不多。

图10-1 运行中的“出题”应用

在“出题”应用中:

  • 用户在表单中输入问题及答案;
  • 显示输入的问题及答案;
  • 将问题及答案存储到网络数据库中。

“答题”应用的功能与之前的“总统问答”类似。事实上,是以“总统问答”为起点创建“答题”应用。不同的是,这里的问题来源于“出题”应用,需要从网络数据库中读取。

学习要点

“总统问答”是一个典型的使用静态数据的应用:问题已经被固化在程序中(称为硬编码),因此,无论用户做过多少次测验,问题都是一样的。新闻类应用、博客以及像Facebook和Twitter这样的社交网络应用,它们所处理的是动态数据,这意味着数据随时在改变。通常这种动态信息是由用户生成的,这类应用允许用户输入、修改并共享信息。在“出题”与“答题”应用中,你将学习如何创建一个用户生成并共享数据的应用。

在第9章的“木琴”应用中,我们首次引入了动态列表概念:用户输入的音符被记录在列表中。这类由用户生成数据的应用更为复杂,而且编程的过程也更加抽象,因为没有预设的静态数据可供参照。尽管可以定义列表变量,但不能设置具体的列表项。在编写程序的同时,还要设想列表中可能存在的数据——那些由最终用户输入的数据。

本章涵盖了App Inventor中的如下内容。

  • 输入表单:允许用户输入信息。
  • 使用相互关联的列表,在遍历列表循环中显示来自多个列表的数据项。
  • 永久保存数据:“出题”应用将问题和答案保存到网络数据库中,“答题”应用将从同一个数据库中读取这些数据。
  • 数据共享:使用网络数据库组件(而非之前的本地数据库组件),将数据存储在网络数据库中。

准备开始

登录App Inventor网站,创建名为“出题”的新项目,将屏幕标题设为“出题”,并连接到测试设备或模拟器。

设计组件

在设计视图中创建“出题”应用的用户界面,完成之后应该如图10-2所示(图的后面有更详细的说明)。

图10-2 设计视图中的“出题”应用

表10-1中列出了应用中的所有组件,用它们来创建图10-1中的用户界面。从组件面板中拖出组件,放在预览窗口中, 将组件名称修改为表中的名称。注意,用于显示标题的标签可以不用重新命名,就用它们的默认值(标签1—标签3),因为在编程视图中不会使用这些名称。

表10-1 “出题”应用中的所有组件

组件类型 所属类别 名称 作用
表格布局 组件布局 表格布局1 放置输入框组件,包括问题输入框及答案输入框
标签 用户界面 标签1 显示“问题”
文本输入框 用户界面 问题输入框 用户在此输入问题
标签 用户界面 标签2 显示“答案”
文本输入框 用户界面 答案输入框 用户在此输入答案
按钮 用户界面 保存按钮 点击保存问题与答案
标签 用户界面 标签3 显示“测验题及其答案”
标签 用户界面 问题答案标签 显示此前输入的所有问题及答案
网络数据库 数据存储 网络数据库1 在网络数据库中保存问题及答案

按以下方式设置组件的属性。

  1. 设置标签1的显示文本为“问题:”,标签2的显示文本为“答案:”,标签3的显示文本为“试题及其答案”。
  2. 设置标签3的字号为18,并勾选粗体属性。
  3. 设置问题输入框的提示属性为“输入问题”,答案输入框的提示属性为“输入回答”。
  4. 设置保存按钮的显示文本属性为“保存”。
  5. 设置问题答案标签的显示文本属性为“测试题及其答案”。
  6. 将问题输入框、答案输入框以及与它们相关的标签移入表格布局1。

注意查看网络数据库组件的属性,它仅有一个属性,即服务地址(如图10-3所示)。该属性定义了一个网络数据库服务,这是专门为App Inventor的网络数据库组件配置的一个服务,用来存储那些用于共享的数据。在默认情况下,网络服务的地址指向MIT App Inventor团队搭建的服务器,网址为http://appinvtinywebdb.appspot.com 。你可以在本书的应用中使用该服务,不过需要说明的是,每个学习使用App Inventor的用户都在使用这个服务,他们将信息存储在同一个网络服务中,也就是说,你存储的数据可能会被所有人看到,也有可能被某些人删除或改写。

默认的服务仅用于学习者测试程序之用,而搭建一个自己的网络服务也不是一件难事(而且免费),尤其是当你想公开发布自己的应用时,建立一个专属的数据库服务则势在必行。现在,你可以继续使用这个默认的服务完成本课程,当你想搭建自己的网络服务时,可参见24.3节的相关说明。

图10-3 设置网络数据库的网址属性

为组件添加行为

和“总统问答”应用类似,我们首先定义了两个全局变量问题列表及答案列表,用来保存事先设定好的问题及答案,但在本章中,不存在预设的问题和答案。

创建空列表

我们同样需要两个列表,不过这里只需要创建两个空列表,如图10-4所示。

图10-4 设问题及答案列表为空列表

由于“出题”与“答题”应用中,所有问题及答案都将由用户自行输入(即所谓的动态的、用户生成的数据),因此,这里用“空列表”块代替“列表”块,来初始化两个列表变量。

记录用户的输入

首先来处理用户的输入行为。具体来说,当用户输入问题和答案并点击保存按钮时,使用“向列表()添加项()”块来向问题列表及答案列表添加新项。代码块如图10-5所示。

图10-5 向列表中添加新输入的项

块的作用

向列表中添加项,意味着向列表的末尾追加新项。如图10-5所示,程序从问题输入框和答案输入框中获取用户输入的内容,并分别追加到相应的列表中。

向列表中添加项,这一操作的结果是更新了列表变量——问题列表及答案列表,但用户看不到这一变化。第三行的代码块用来显示这一变化:用冒号将两个列表的内容连接起来。默认情况下,App Inventor用小括号来包围列表内容,列表项之间用空格间隔,像这样:(列表项1 列表项2 列表项3)。当然,这不是显示列表的理想方式,只是暂时用来测试程序的执行结果。稍后我们将用更加高级的方式来显示列表,即每行只显示一组问题及答案。

清空问题及答案

回忆一下在“总统问答”应用中,当转换到下一题时,要清空上一题用户输入的答案。在本应用中,当用户提交了一组问题及答案后,同样要清空问题输入框及答案输入框,以便为下一题的输入做好准备,如图10-6所示。

图10-6 保存用户输入的问题及答案后清空输入框

测试:输入一组问题及答案,检查程序的运行结果。输入新内容后,它们是否显示在问题答案标签中?

块的作用

用户输入的问题及答案将分别被添加到各自的列表中,并显示出来,此时问题输入框及答案输入框中的文本被清空。

用多行文本显示问题及答案

现在是以App Inventor默认的格式来显示应用中的列表——问题列表及答案列表。假设有一个关于中国各省省会{![原著中以美国的州首府为例,为了便于读者的阅读理解,这里改为中国的省会。——译者注]}的测验,已经输入了两组问题及答案,则显示成:

(黑龙江省的省会在哪儿?广东省的省会在哪儿?):(哈尔滨 广州)。

对于应用的设计者来说,这显然不是一种理想的数据显示方式。理想的方式应该是每行只显示一组问题及答案,像下面这样:

黑龙江省的省会在哪儿? 哈尔滨
广东省的省会在哪儿? 广州

本书第20章专门针对单个列表讲述了逐行显示列表项的方法,在继续学习之前,可以去阅读一下。

这里的任务稍显复杂,因为涉及两个列表。为了应对这种复杂性,需要创建一个过程,取名为“显示问题及答案”,将显示数据的代码放在这个过程里,并在保存按钮的点击事件处理程序中调用该过程。

为了实现逐行显示每一组问题及答案,需要完成以下步骤:

  • 使用“针对列表()中的每一(项)”块遍历问题列表中的每一个问题;
  • 使用“答案”及“答案索引值”变量,以便在遍历问题的同时,获取与问题相对应的答案;
  • 使用拼字串块连接每一组的问题及答案,并用换行符(\n)来实现每一组问题及答案之间的分行显示。

具体代码如图10-7所示。

图10-7 显示问题及答案过程

块的作用

显示问题及答案过程封装了所有用于显示数据的块。

有了这样一个过程,当程序中不止一处需要显示列表数据时,就可以直接调用该过程,而无需多次复制这些用于显示列表数据的代码块。

由于遍历列表的块只能遍历一个列表,而本应用中有两个列表——问题列表及答案列表,因此要在遍历问题列表的同时,为每个问题选择对应的答案。这需要定义一个索引值变量,就像第8章“总统问答”中的“当前问题索引值”一样。这里定义了“答案索引值”,当遍历问题列表时,用来跟踪与问题对应的答案在答案列表中的位置。

在开始遍历问题列表之前,设答案索引值为1。在遍历问题列表过程中,答案索引值用来从答案列表中选择与当前问题相对应的答案,然后索引值递增1。在遍历过程的每一次迭代中,当前一组问题及答案被添加到问题答案标签的最后一行,问题与答案之间以冒号分隔。

调用显示问题及答案过程

我们已经创建了“显示问题及答案”过程,但是如果没有其他程序调用它,它不会起到任何作用。修改保存按钮的点击事件处理程序,用“显示问题及答案”过程代替对问题答案标签的简单设置,来显示所有的问题及答案。更新后的块如图10-8所示。

图10-8 调用“显示问题及答案”过程来替代原有代码

测试:输入一组问题及答案来测试程序的运行效果。添加新的问题答案后,它们是否显示在问题答案标签中?

将数据永久保存到网络数据库中

到目前为止,用户输入的问题及答案只是保存在列表中,如果此时用户退出应用,会怎么样呢?正如在“开车不发短信”(第4章)或“安卓,我的车在哪儿?”(第7章)中所学到的,如果数据不能存储到数据库中,那么当用户退出并重新打开应用时,数据将丢失。只有永久存储数据,才能让出题者在每次打开应用时,都能看到最新版本的数据,并对数据内容进行编辑。同时,永久保存数据也是必要的,因为在“答题”应用中也需要访问这些数据。

之前我们学习过用本地数据库组件存储并提取数据,本章将使用网络数据库组件。两者的区别是:前者将数据存储在手机上,而后者将数据存储在网络数据库中。

本应用在设计上之所以选择网络数据库而非本地数据库,关键在于这些被保存的数据要供两个应用访问,如果出题者把问题和答案都存储在个人的手机上,那么答题者将无法获取数据并参加考试。而网络数据库组件将数据保存在互联网上,答题者可以使用任何一部可以上网的安卓设备来访问试题及答案。(网络数据存储也经常被称作云存储。)

下面是永久保存列表数据(如问题及答案)的通用方案:

  • 每当向列表中添加新项时,将数据保存到数据库;
  • 应用启动时,从数据库中加载数据,并保存在列表变量中。

首先考虑数据的保存:每次用户输入新的问题及答案时,将问题列表及答案列表保存到数据库中。

块的作用

网络数据库组件的保存数据块将数据存储在网络数据库中。保存数据块有两个参数:标记和数值,其中标记用作数据的标识,数值是要保存的实际数据。如图10-9所示,问题列表在存储时以“问题”为标记,而答案列表则用“答案”作为标记。

图10-9 将问题及答案保存到数据库中

建议在个人应用中使用更有特点的标记(如大卫的问题及大卫的答案)来代替问题和答案。这非常重要,因为你正在使用App Inventor默认的网络数据库,所以你的数据(问题列表及答案列表)有可能被其他人的数据覆盖,也包括那些正在学习本章课程的人。

测试:对于网络数据存储的测试,不同于以往应用中进行的测试,因为你的应用已经跨越出安卓设备的边界,与App Inventor默认的网络数据库服务发生了关联。在测试设备中输入问题及答案,然后在电脑中打开浏览器,输入网址http://appinvtinywebdb.appspot.com ,点击“getvalue”并输入一项数据的标记(如“问题”或“答案”),如果一切正常,网页上将显示你存储过的问题或答案。

这里再次提醒各位,App Inventor默认的网络数据库服务是一个开放的服务,使用App Inventor的程序员以及他们创建的各类应用共享该服务。如果你打算正式发布一款应用,则需要建立自己私有的数据库服务。幸运的是,实现这一点很简单,而且不需要编程(参见24.3节)。

从数据库加载数据

本应用需要永久保存数据,一方面是从出题者的角度考虑,当出题者输入了一些问题及答案后,他可以随时关闭应用,当再次打开应用时,此前输入的内容不会丢失;另一方面,答题者也可以获取这些已保存问题及答案。我们先来解决出题者的问题,在每次启动应用时,从网络数据库中读取那些已保存的数据,稍后再解决答题者的问题。

正如我们之前所学,在应用启动时需要执行的操作,要放在Screen1的初始化事件处理程序中。在本应用中,需要使用网络数据库组件向网络数据库发出数据请求(以“问题”及“答案”为标记),因此初始化程序将两次调用该组件的请求数据功能。具体代码如图10-10所示:

图10-10 应用启动时向网络数据库请求数据,并显示收到的数据

块的作用

图10-10中使用的网络数据库组件的“请求数据”块,与之前用过的本地数据库的“请求数据”块的运行机制不同,后者会立即返回一个值,而前者只负责向网络数据库发送请求,不会立即获得返回值。

当应用收到网络数据库返回的数据时,会触发“获得数据”事件,因此需要另外编写一个获得数据事件的处理程序来接收返回的数据。

网络数据库的获得数据事件中携带了两个参数:数据标记及数据。顾名思义,数据标记就是发出数据请求时使用的标记(本应用中为“问题”或“答案”),而数据则是所请求的数据本身(本应用中为问题列表或答案列表)。

如上所述,在本应用的Screen1初始化程序中,共发出了两次数据请求,分别以问题及答案为标记请求保存在网络数据库中的问题列表及答案列表,因此,数据也将分两次返回,即获得数据事件将被触发两次。为了避免把问题列表中的数据写入答案列表中(反之亦然),需要对返回的数据标记进行检查,来判断收到的数据来自于哪个请求,然后再把返回值写到相应的列表中(问题列表或答案列表)。

在获得数据事件的处理程序中,外层使用了“如果...则”块,用来判断返回的数据是否为列表。这个判断主要是为了排除两种情况:(1)当应用第一次启动时,数据库中没有数据,因此返回值为空;(2)虽然不是第一次启动应用,但用户不曾在数据库中保存过问题及答案的数据。如果收到的数据是列表,说明的确有数据返回,则对数据进行后续处理,否则,不做任何处理。

如果返回的数据是列表,则对返回的数据标记进行判断,以便确定是哪一次请求返回的数据。如果数据标记是“问题”,则将返回的数据保存到全局变量问题列表中,否则,保存到答案列表中。(如果你使用的数据标记不是“问题”和“答案”,那么请替换成你自己的标记,然后再做判断。)

我们希望当两次请求的数据都收到时(获得数据事件被触发两次)再来显示这些数据。想想看,如何判断从数据库收到了两个列表的数据?是的,用“如果...则”块来判断两个列表的长度是否相等,因为只有当两次请求的数据都收到时,两个列表才均不为空,判断结果才能为真。如果为真,你可以轻松地调用之前编写的显示问题及答案过程来显示收到的数据。

完整的“出题”应用

图10-11中显示了“出题”应用中的完整代码。

图10-11 “出题”应用中的代码块

“答题”应用:从数据库中读取试题

“出题”应用已经就绪,下面来创建“答题”应用,一个可以动态加载试题的应用,相当简单。只要在第8章“总统问答”应用的基础上稍加修改即可(如果你还没有学完第8章,马上就去学,然后再继续)。

在App Inventor中打开“总统问答”应用,选择“另存应用”将应用另存为“答题”。这保证了在不修改“总统问答”应用的前提下,以此为基础来构建“答题”应用 。

下面,在设计视图中做如下修改。

  1. 这个版本的“答题”应用中,不需要为问题匹配图片,因此首先删除所有与图片相关的部分:在设计视图中,在素材区域中选择并删除所有图片,然后再删除图片1组件,这将删除编程视图中对它的所有引用(编程视图中的global 图片列表需手动删除)。
  2. 由于“答题”应用中会用到数据库中的数据,因此添加一个网络数据库组件。
  3. 在试题被加载完成之前,不希望用户来回答问题或点击“下一题”按钮,因此取消勾选回答按钮和下一题按钮的启用属性。

现在,回到编程视图中修改代码,以便从数据库中加载试题。首先,由于应用中不再有固定的问题及答案,因此删除问题列表及答案列表中的“列表”块,以及其中的所有问题及答案文本。改过的代码如图10-12所示。

图10-12 应用启动时,设问题列表及答案列表为空列表

删除图片列表,与“出题”应用中相同,修改Screen1的初始化程序,两次调用网络数据库组件的请求数据功能,来加载问题及答案列表。具体代码如图10-13所示。

图10-13 向网络数据库请求试题及答案

最后,拖出一个网络数据库组件的获得数据事件块,编写获得数据事件处理程序。与“出题”应用中的程序相类似,但这里只显示第一个问题,而且没有答案。先尝试自己做些修改,然后对照图10-14,看看你的方法是否与图中的相符。

图10-14 在获得数据事件中处理来自网络的数据

块的作用

应用启动时将触发Screen1的初始化事件,应用从网络数据库中请求试题及答案数据。每次(共两次)收到数据都会触发网络数据库组件的获得数据事件。首先使用“是列表”块来判断返回的数据是否为列表:如果是,则使用数据标记来判断数据来自于哪一个请求,并将数据写入相应的列表中。如果已经收到问题列表,则从列表中选择第一题并显示;如果已经收到答案列表,则启用回答按钮及下一题按钮,以便用户可以开始答题。

以上是针对“答题”应用的修改。如果你在“出题”应用中添加了一些问题及答案,之后启动“答题”应用,那么这些问题将逐一显示在“答题”应用中。

完整的“答题”应用

图10-15中显示了“答题”应用中的全部代码。

图10-15 “答题”应用中的全部代码块

改进

在“出题”与“答题”应用开始运行之后,你也许会尝试做一些改进。举例如下。

  • 允许出题者为每个问题指定一张图片。这个功能稍显复杂,由于网络数据库中无法保存图片,因此图片只能来源于网络。需要在应用中添加一个文本输入框(放在答案输入框下方),出题者在其中输入图片的网址,然后编写代码,将图片组件的图片属性动态地设置为图片的网址。
  • 允许出题者从问题和答案列表中删除某些列表项。在应用中添加一个列表选择框组件,用户可以从中选择要删除的问题,并使用删除列表项块删除选中项(记住要同时从两个列表中删除,并更新数据库)。有关列表选择框以及删除列表项的相关内容,请参见第19章。
  • 让出题者为他的试题设定一个名称。试题名称也需要保存到数据库中(保存时需要设置数据标记),并在“答题”应用中,与试题一同加载。名称加载完成后,可以将其设置为Screen1的标题属性,这样当用户答题时,可以看到试题的名称。
  • 允许创建多个不同名称的试题。需要建一个测验的列表,并且用试题名称作为保存问题和答案时的标记(或标记的一部分)。

小结

以下是本章涵盖的内容。

  • 动态数据是指由用户输入的或从数据库中加载的信息。用动态数据编程会更加抽象。更多信息请参见第19章。
  • 可以使用网络数据库组件来永久保存数据。
  • 从网络数据库中提取数据,需要两个步骤:首先,使用网络数据库组件的请求数据功能,向数据库发出请求,并等待数据的返回;其次,当数据返回时,触发网络数据库组件的获得数据事件,在该事件的处理程序中,可以把数据存储在列表中,或以其他方式进行处理。
  • 网络数据库中的数据可以在多部手机和应用之间共享。关于网络数据库的更多信息,请参见第22章。

注释:

表单:英文为form,如果译为“表格”则更容易被中国人理解,“表单”是程序员的说法。当你去银行申请开户,或者申请一份工作,或者申请加入某个组织时填写的表格,就是这里所说的“表单”。

TnyWebDB的服务器地址:目前书中提供的网址在国内访问存在问题,因此有国内志愿者搭建了一个可供学习者访问的网络服务,网址是tinywebdb.17coding.net。国内用户访问该网址后,点击getvalue后进入查询数据页面,输入标记即可查询相应的数据。