第8章的“總統測驗”可以被定制成各種測驗,但這種定制只對App Inventor程序員有用。只有程序員可以修改問題和答案,而對于父母、老師或其他用戶來說,他們無法創建一個測驗或變換問題(除非他們也學App Inventor!)。
本章將構建一個“出題”應用,“老師”可以在輸入表單中創建試題。試題和答案將被存儲在Web數據庫中,以便“學生”可以單獨訪問“答題”應用并參加考試。通過創建這兩個應用,你會在概念上產生更大的飛躍,并學習如何創建一個應用,讓用戶自行生成數據,并實現用戶之間跨應用的數據共享。

“出題”與“答題”這兩個應用協同工作,讓“老師”可以為“學生”出題。父母可以在長途旅行中做一些旅行花絮類的應用,以增加孩子們的樂趣;小學教師可以創建“數學突擊”一類的小測驗;而大學生們可以創建一系列的測驗,幫助他們的學習小組來準備期末考試。本章建立在第8章“總統測驗”的基礎上,如果你還沒學過,在繼續本章之前,請先學習第8章。
本章將設計兩個應用:針對“老師”的“出題”應用(見圖10-1)以及針對“學生”的“答題”應用。在“出題”應用中:
* 用戶在輸入表單中輸入問題及答案;
* 顯示輸入的一對問答;
* 將問題及答案存儲在數據庫中。

**圖 10-1 出題應用**
“答題”應用的功能與之前的“總統測驗”類似。事實上,是以“總統測驗”為起點創建“答題”應用,不同的是,這里的問題是使用“出題”應用輸入并保存在數據庫中的。
## **學習要點**
“總統測驗”是一個使用靜態數據的應用范例:不管用戶做多少次測驗,問題都是一樣的,因為問題被寫在程序中(稱為“硬編碼”)。新聞應用、博客以及像Facebook和Twitter這類的社交網絡應用采用的是動態數據,這意味著數據隨時在改變。通常這種動態信息由用戶生成,這類應用允許用戶輸入、修改并共享信息。在“出題”與“答題”應用中,將學習創建一個應用,來處理用戶生成的數據。
在第9章“木琴”應用中,我們首次引入動態列表概念:用戶輸入的音符被記錄在列表中。由用戶生成數據的應用更為復雜,而且使用的塊也更抽象,因為沒有預設的靜態數據可供參照。盡管可以定義列表變量,但不能設置具體的項。在編寫程序的同時,需要設想最終用戶輸入的數據被添加到列表中。
本章涵蓋了App Inventor中的如下內容:
* 輸入表單:允許用戶輸入信息;
* 顯示來自多個列表的數據項;
* 永久保存數據:“出題”應用將問題和答案保存到網絡數據庫中,“答題”應用將從同一個數據庫中加載它們;
* 數據共享:使用TinyWebDB組件(而不是之前的TinyDB)將數據存儲在Web數據庫中。
## **準備開始**
登陸App Inventor網站,創建新項目“MakeQuiz”,屏幕標題設為“出題”,并連接到測試手機或模擬器。
## **設計組件**
使用組件設計器來創建用戶界面,如圖10-2所示(圖的后面有更詳細的說明),組件清單列于表10-1中。從Palette中拖出組件,將名稱改為表中的命名。注意,標題Label的名稱(Label1 – Label3)不必改,就用它們的默認值(因為在編輯器中不會使用這些名稱)。
**表10-1 “出題”應用中的所有組件**
| 組件類型 | 面板中分組 | 命名 | 作用 |
| --- | --- | --- | --- |
| TableArrangement | Layout | TableArrangement1 | 格式化表單,包括問題及答案|
| Label | User Interface | Label1 | 提示“問題:” |
| TextBox | User Interface | QuestionText | 用戶在此輸入問題 |
| Label | User Interface | Label2 | 提示“答案:” |
| TextBox | User Interface | AnswerText | 用戶在此輸入答案 |
| Button | User Interface | SubmitButton | 用戶點擊提交問題-答案對兒 |
| Label | User Interface | Label3 | 顯示“測驗的問題及答案。” |
| Label | User Interface | QuestionAnswersLabel |顯示之前輸入的成對的問題答案|
| TinyWebDB | Storage | TinyWebDB1 | 用數據庫保存并提取數據 |

**圖 10-2 組件設計器中的“出題”應用**
按以下方式設置組件屬性:
1\. 設置Text屬性:Label1為“問題:”,Label2為“答案:”,Label3為“試題及答案”;
2\. 設置Label3的字號為18,并勾選FontBold屬性;
3\. 設置QuestionText的Hint屬性為“輸入問題”,AnswerText的Hint屬性為“輸入回答”;
4\. 設置SubmitButton的Text屬性為“提交”;
5\. 設置QuestionsAnswersLabel的Text屬性為“試題及答案”;
6\. 將QuestionText、AnswerText以及與它們相關的Label移入TableArrangement1。
## **為組件添加行為**
在“總統測驗”中,首先定義了兩個全局列表變量QuestionList和AnswerList,本章中無需為這兩個變量提供預設的問題和答案,如圖10-3所示。

**圖 10-3 列表變量初始化**
需要注意,與“總統測驗“不同的是,這兩個列表沒有定義列表項,因為“出題”及“答題”應用中,所有數據都將由用戶創建(即動態的、用戶生成的數據)。
### **記錄用戶的輸入**
首先來處理用戶的輸入行為。具體來說,當用戶輸入問題和答案并點擊提交時,程序要向列表中添加數據項來更新QuestionList和AnswerList,如下圖所示:

**圖 10-4 向列表中添加新項**
#### **塊的作用**
向列表中添加項,意味著向列表的末尾追加新項。如圖10-4,程序從QuestionText和AnswerText文本框中獲取用戶輸入的內容,并分別被追加到相應的列表中。
向列表中添加的項更新了列表變量QuestionList和AnswerList,但用戶看不到任何變化。第三行的塊用來顯示這個變化:用冒號將兩個列表的內容連接起來。默認情況下,App Inventor用小括號來包圍列表內容,列表項之間用空格間隔,像這樣:(item1 item2 item3)。當然,這不是顯示列表的理想方式,只是暫時用來測試程序的行為。稍后我們將用更高級的方式來顯示列表,即,每對問題答案各占一行。
### **清空問題及答案**
回憶一下在“總統測驗”中,當移動到下一題時,要清空上一題的回答結果。在本應用中,當用戶提交了一對問題-答案后,同樣要清空QuestionText及AnswerText文本框,以便準備下一題的輸入,如下圖所示:

**圖 10-5 提交問題-答案之后清空文本框**
#### **塊的作用**
用戶提交的問題-答案,將分別被添加到各自的列表中,并顯示出來,這時QuestionText和AnswerText中的文本被清空,如圖10-5所示。請注意,可以復制一個有內容的文本塊(如上圖中的“:”塊),通過刪除塊中的文本,來獲得一個空的文本塊。
### **用多行文本顯示問題-回答**
現在是以App Inventor的默認格式來顯示問題及答案。假如有一個有關州首府的測驗,已經輸入了兩對問題-答案,則顯示成: (加州首府在哪? 紐約州首府在哪?):(薩克拉門托 奧爾巴尼)。
可以想像,如果測驗中的問題很多,結果會顯得非常混亂。理想的顯示方式,應該是每行只顯示一對問題-答案:
> 加州首府在哪? 薩克拉門托
>
> 紐約州首府在哪? 奧爾巴尼
第20章講述了單個列表中項的逐行顯示技術,在繼續學習之前,可以去閱讀一下。
這里的任務稍顯復雜,因為涉及到兩個列表。為了應對這種復雜性,需要創建過程displayQAs,并從SubmitButton.Click事件處理程序中調用該過程。
逐行顯示問題-答案,需要做到以下幾點:
* 使用foreach塊遍歷QuestionList中的每個問題;
* 使用變量answerIndex,在遍歷問題的同時,獲取與問題對應的答案;
* 使用join塊連接每對問題-答案,并用換行符(\n)來分開每對問題-答案,如下圖所示:
#### **塊的作用**
過程displayQAs封裝了所有用于顯示數據的塊,如圖10-6所示,在需要顯示列表時,可直接調用displayQAs,而不必再重復使用過程內部的塊。

**圖 10-6 創建displayQAs過程**
由于foreach塊只能遍歷一個列表,而本應用中有兩個列表,因此要求在遍歷問題列表的同時,為每個問題選擇對應的答案。這需要定義一個索引變量,就像第8章“總統測試”中的currentQuestionIndex一樣,這里定義了answerIndex,當foreach遍歷QuestionList時,用來跟蹤對應的答案在列表AnswerList中的位置。
在foreach開始遍歷之前,設answerIndex的值為1;在foreach遍歷過程中,answerIndex用來從AnswerList中選擇當前問題的答案,然后遞增1。在foreach的每次迭代中,當前的問題-答案被添加到QuestionsAnswersLabel的最后一行,問題與答案之間以冒號分隔。
### **調用新建的過程**
已經創建了顯示問題-答案的過程displayQAs,但在調用它之前,它起不到任何作用。修改SubmitButton.Click事件處理程序,用displayQAs替代對QuestionsAnswersLabel.Text的簡單設置,來顯示所有的問題-答案。更新后的塊如圖10-7所示。

**圖 10-7 在SubmitButton.Click中調用displayQAs過程**
### **將數據永久保存到Web數據庫**
到目前為止,用戶輸入的問題-答案只是保存在列表中,如果此時用戶退出應用,會怎么樣呢?正如“開車不發短信”(第4章)或“Android,我的車在哪兒?”(第7章)中所學到的,如果數據不能存儲到數據庫中,那么當用戶退出并重新打開應用時,數據將丟失。只有永久存儲數據,才能讓出題者在每次打開應用時,都能看到最新版本的數據,并對數據內容進行編輯。同時,永久保存數據也是必要的,因為在“答題”應用中也需要訪問這些數據。
之前我們學習過用TinyDB組件在數據庫中存儲并檢索數據,本章將使用TinyWebDB組件。兩者的區別是:TinyDB將數據存儲在手機上,而TinyWebDB將數據存儲在Web數據庫中。
本應用在設計上之所以選擇在線數據庫,而非手機數據庫,關鍵在于存這些數據要供兩個應用訪問,如果出題者把問題和答案都存儲在個人的手機上,那么答題者將無法獲取數據并參加考試!而TinyWebDB將數據保存在互聯網上,答題者可以使用不同于出題者的設備來訪問試題及答案。(在線數據存儲通常被稱作云。)
下面是永久保存列表數據(如問題及答案)的通用方案:
* 每當向列表中添加新項時,將數據保存到數據庫;
* 應用啟動時,從數據庫中加載數據,并保存在列表變量中。
首先考慮數據的保存:每次用戶輸入新的問題-答案時,將QuestionList和AnswerList保存到數據庫中。
#### **塊的功能**
TinyWebDB1.StoreValue塊將數據存儲在Web數據庫中。StoreValue有兩個參數:tag用做數據的標識,value是要保存的實際數據。如圖10-8所示,QuestionList在存儲時以“questions”為tag(標簽),而AnswerList則用“answers”為tag(標簽)。

**圖 10-8 將問題和答案保存到數據庫中**
建議在個人應用中使用更有特點的tag(如DavesQuestions和DavesAnswers)來代替questions和answers,這非常重要,因為你正在使用App Inventor的默認Web數據庫,所以你的數據(列表questions和answers)可能會被別人的數據覆蓋,也包括那些正在學習本教程的人。
這里要提醒各位,App Inventor默認的Web服務會在各個程序員和各種應用之間共享,因此它僅適用于測試。當你打算正式發布一款應用時,需要建立自己私有的數據庫服務。幸運的是,做到這一點很簡單,而且不需要編程(見第22章)。
### **從數據庫加載數據**
本應用需要永久保存數據,一方面因為出題者可以隨時關閉應用,并隨時啟動應用,以便對之前輸入問題和答案進行補充、修改或刪除。這就需要在每次啟動應用時,從數據庫中加載那些已存儲的數據。(另一方面,答題者可以訪問數據庫總的問題及答案,稍后會涉及到。)
正如我們之前所學,在應用啟動時需要進行的操作,要通過Screen.Initialize事件處理程序來實現。在本應用中,需要用TinyWebDB組件向Web數據庫請求questions及answers這兩個列表,因此Screen1.Initialize將兩次調用TinyWebDB.GetValue。塊的設置如下圖:

**圖 10-9 在應用啟動時從數據庫中請求列表數據**
#### **塊的功能**
圖10-9中使用的TinyWebDB.GetValue塊與之前用過的TinyDB.GetValue的運行機制不同,后者會立即返回一個值,而前者只負責向Web數據庫發送請求,不會立即收到返回值。當應用收到Web數據庫返回的數據時,會觸發TinyWebDB.GotValue事件,因此需要另外編寫一個GotValue事件的處理程序來接收返回的數據。
當TinyWebDB.GotValue事件發生時,所請求的數據封裝在參數valueFromWebDB中,所請求的數據標簽(tag)則封裝在參數tagFromWebDB中。
如圖10-9所示,在Screen1.Initialize事件處理程序中發出了兩次GetValue請求,分別請求questions和answers,因此GotValue也將被觸發兩次。為了避免把questions的數據寫入AnswerList中(或反過來),需要對tag進行檢查,來判斷收到的是哪個請求的返回值,然后再把返回值寫到相應的列表中(QuestionList或AnswerList)。現在,你該意識到這些tag的真正用途了吧!
#### **塊的功能**
應用中兩次調用TinyWebDB1.GetValue來請求存儲過的數據:分別是為QuestionList及AnswerList。當收到Web數據庫返回的數據時,觸發TinyWebDB1.GotValue事件,如圖10-10。

**圖 10-10 當收到來自web的數據時觸發GotValue事件**
從數據庫中返回的數據封裝在GotValue事件的valueFromWebDB參數中。在GotValue的事件處理程序中,外層的if塊用來判斷數據庫的返回值valueFromWebDB是否為空。設想用戶首次啟動應用,數據庫中沒有任何數據。通過詢問參數valueFromWebDB是否“is a list?”,可以得知是否真的有數據返回。如果沒有數據返回,則會跳過對GotValue事件的處理。
如果有數據返回(is a list?為真),再繼續判斷收到的是哪個請求。識別數據的標記tag封裝在tagFromWebDB中:即可能是“questions”,也可能是“answers”,如果tag是“questions”,則將valueFromWebDB保存到變量QuestionList,否則(else塊),保存到AnswerList。(如果你使用的tag不是“questions”和“answers”,請替換成你自己的tag再做判斷。)
我們希望當兩個列表都已收到時(GotValue被觸發兩次)再來顯示這些數據。想想看,如何判斷從數據庫收到了兩個列表的數據?是的,用if塊來檢測兩個列表的長度是否相同,因為只有兩個列表都收到了,檢測結果才能為真。如果為真,你可以輕松調用之前編寫的displayQAs過程來顯示加載的數據。

**圖 10-11 “出題”應用中的塊**
## **答題:從數據庫中讀取試題的應用**
“出題”應用已經就緒,下面來創建“答題”應用,一個可以動態加載測驗的應用,相當簡單。只是在“總統測驗”的基礎上稍加修改(如果你還沒學過,現在就去學,然后再繼續)。
打開“總統測驗”,選擇“save project as”將應用另存為“TakeQuiz”,這保證在不修改“總統測驗”的情況下,以此為基礎來構建“答題”應用 。
### **在組件設計器中調整組件**
在組件設計器中做如下改變:
1\. 這個版本的“出題/答題”應用不需要為問題搭配圖片,因此首先刪除所有與圖片相關的部分:在組件設計中,從Media區域中選擇并刪除所有圖片,然后再刪除Image1組件,這將刪除塊編輯器中對它的所有引用(塊編輯器中的global PictureList需手工刪除);
2\. 由于“答題”應用中會用到數據庫中的數據,因此添加一個TinyWebDB組件;
3\. 在試題被加載完成之前,不希望用戶來回答問題或點擊“下一題”按鈕,因此取消勾選“提交”和“下一題”按鈕的Enabled屬性。
### **在快編輯器中編程:從數據庫加載測驗**
首先,修改列表變量的初始化設置:這里不需要預置問題及答案,因此用create empty list塊替代QuestionList和AnswerList初始化時用到的make a list塊,結果如圖10-12所示。

**圖 10-12 應用開始時,問題及答案列表為空**
其次,刪除PictureList,這里不需要圖片;修改Screen1.Initialize,兩次調用TinyWebDB.GetValue來加載列表,與“出題”應用中相同,如圖10-13所示。

**圖 10-13 從Web數據庫中請求問題及答案列表**
最后,拖出一個TinyWebDB.GotValue事件處理程序。此事件處理程序與“出題”應用中的程序類似,但這里只顯示第一個問題,而且沒有答案。先嘗試自己做些修改,然后對照圖10-14,看看你的方案是否與圖中的相符。

**圖 10-14 用GotValue處理來自Web的數據**
#### **塊的作用**
應用啟動時觸發Screen1.Initialize事件,應用從Web數據庫請求數據(questions及answers)。每次(共兩次)收到數據都會觸發TinyWebDB.GotValue事件。首先使用“is a list?”來判斷valueFromWebDB中是否真的含有數據:如果有,則使用tagFromWebDB來判斷是哪個請求返回的,并將valueFromWebDB值寫入相應的列表中。如果QuestionList已經加載,則從QuestionList選擇第一題并顯示;如果AnswerList已經加載,則啟用“提交”及“下一題”按鈕,以便用戶可以開始答題。
### **完整的答題應用**

**圖 10-15 “答題”應用中塊的最終設置**
## **改進**
在“出題”與“答題”應用開始運行之后,你也會會嘗試做一些改進。例如:
* 允許出題者為每個問題指定一個圖片。當然,你(作為開發者)不能預加載這些圖像,并且目前應用的用戶也無法做到這一點。因此,圖片必須是一些Web URL,出題者需要在“出題”應用的表單中輸入這些URL,這些URL構成了應用的第三個列表。請注意,你可以將Image組件的Picture屬性設置為一個URL。
* 允許出題者從問題和答案列表中刪除項。用戶可以使用ListPicker組件選擇一個問題,并用remove list item塊來刪除列表項(記住要同時從兩個列表中刪除,并更新數據庫)。想獲得ListPicker和列表刪除的相關幫助,請參見第19章。
* 讓出題者為他的測驗設定名稱。測驗名稱也要以一個不同的tag保存到數據庫中,并在“答題”應用中,與整個測驗一同加載。名稱加載完成后,可以將其設置為Screen1的Title屬性,這樣當用戶答題時,測驗名稱也將顯示出來。
* 允許創建多個不同名稱的測驗。需要建一個測驗的列表,并且用測驗的名稱作為保存問題和答案時的tag(一部分)。
## **小結**
以下是本章涵蓋的內容:
* 動態數據是指由用戶輸入的、或從數據庫中加載的信息。用動態數據編程會更加抽象。更多信息請參見第19章;
* 可以使用TinyWebDB組件在Web數據庫中永久保存數據;
* 要從Web數據庫中檢索數據,需要用TinyWebDB組件的GetValue方法。當Web數據庫返回數據時,會觸發TinyWebDB.GotValue事件。用TinyWebDB.GotValue事件處理程序,可以把數據存儲在列表中,或以其他方式進行處理;
* TinyWebDB數據可以在多部手機和應用之間共享。關于(Web)數據庫的更多信息,請參見第22章。
- 簡介
- 序言
- 前言
- 第 1 章 Hello 貓咪
- 第 2 章 油漆桶
- 第 3 章 打地鼠
- 第 4 章 開車不發短信
- 第 5 章 瓢蟲快跑
- 第 6 章 巴黎地圖旅游
- 第 7 章 安卓,我的車在哪?
- 第 8 章 總統測驗
- 第 9 章 木琴
- 第 10 章 出題及答題
- 第 11 章 廣播中心
- 第 12 章 遙控機器人
- 第 13 章 亞馬遜掌上書店
- 第 14 章 理解應用的結構
- 第 15 章 軟件工程與應用調試
- 第 16 章 應用中的存儲
- 第 17 章 創建動畫應用
- 第 18 章 程序中的決策:條件塊
- 第 19 章 數據列表編程
- 第 20 章 循環
- 第 21 章 定義過程
- 第 22 章 數據庫
- 第 23 章 傳感器
- 第 24 章 與Web API通信