# [**Chapter 4—模塊**](http://docs.nativescript.org/tutorial/chapter-4#chapter-4nativescript-modules)
本章你會了學習 NativeScript 模塊,就是你app的`node_modules/tns-core-modules` 文件夾里的JS模塊。不管你是否意識到,你已經用過一些 NativeScript 模塊了。包括你通過 `require()` 引入的模塊( view, frame 和 observable 模塊)以及那些你在XML里面使用的UI組件( page, image, text field 和 button 模塊)。
## **目錄** {#top}
* [4.1: Connecting to a backend](#4-1)
* [4.2: Dialog module](#4-2)
* [4.3: ListView](#4-3)
* [4.4: Working with arrays](#4-4)
* [4.5: GridLayout](#4-5)
* [4.6: ActivityIndicator](#4-6)
* [4.7: Animations](#4-7)
如果你深入 研究`node_modules/tns-core-modules` 你會了解這些模塊是如何工作的。 我們從找到 `node_modules/tns-core-modules/camera` 文件夾開始,它包含了相機模塊的實現。包括:
* 一個`package.json` 文件 用于設置模塊名稱;
* 一個涵蓋模塊的Android實現的文件 \(`camera.android.js`\);
* 一個涵蓋模塊的IOS實現的文件 \(`camera.ios.js`\);
* 一個涵蓋模塊的androi和IOS實現的共享代碼的文件 \(`camera-common.js`\);
> ### **提示:**
>
> ---
>
> 你可以參考 [Node.js documentation on folders as modules](https://nodejs.org/api/modules.html#modules_folders_as_modules) 尋求更詳細的信息來了解NativeScript 如何組織它的模塊。
\*.ios.\* and \*.android.\*的命名約定看起來和熟悉,它正好和我們在2.4章節里用來引入 Android- 和 iOS-specific 樣式的約定一樣。 NativeScript 使用同樣的約定在 iOS 和 Android 上實現它的模塊。既然你知道這些模塊在哪里了,我們就來好好看看到底它們能為你的app做些啥。
## [**4.1: Connecting to a backend**](http://docs.nativescript.org/tutorial/chapter-4#41-connecting-to-a-backend)連接后臺 {#4-1}
當你在注冊頁面創建帳戶的時候,你或許注意到數據魔術般地跑到……某個地方。事實上這里面并沒有什么魔法;注冊頁面調用了一個 [Telerik Backend Services](http://www.telerik.com/backend-services) 提供的 RESTful 接口來為雜貨鋪服務注冊用戶。
> ### 注意:
>
> ---
>
> 你沒必要一定要用 Telerik Backend Services來支持后臺服務;在 NativeScript app里,你可以使用任何 HTTP API 。 我們在本教程里使用Telerik Backend Services 只是一個習慣,因為它讓我們啟動HTTP端點更快一點。
看一下 `app/shared/config.js`。這里只有一小段代碼片段,但它包含一個硬編碼路徑指向注冊頁面用到的雜貨店后臺,你馬上會使用它:
`module.exports = {`
`apiUrl: "https://api.everlive.com/v1/GWfRtXi1Lwt4jcqK/"`
`};`
> ### 提示:
>
> ---
>
> `config.js`同樣也顯示了一個方便的模式,你可以用于 在你的app里共享配置變量。
接下來,看一下`app/shared/view-models`文件夾,它包含我們預先封裝好的一些視圖模型,這些視圖模型含有連通后臺的代碼。你可以通過`user-view-model.js` 文件的 `register()` 方法來印證。
> ### 注意:
>
> ---
>
> 在大型app里,把與后臺交互的代碼放在單獨的文件中是很常見的,而不是直接放在視圖模型里。但在本例,為了簡單,交互代碼直接放在了視圖模型里——小型app完全有理由這么做。
注意 `register()` 方法使用 config 模塊獲取到后臺路徑,以及 [fetch](http://docs.nativescript.org/cookbook/fetch) 模塊發起HTTP請求。
`var config = require("../../shared/config");`
`var fetchModule = require("fetch");`
NativeScript的 fetch 模塊使用和 [browser's new fetch\(\) API](https://fetch.spec.whatwg.org/) 相同的API。因此,如果你已經知道如何使用web的 `fetch()` 方法,你就已經明白在 NativeScript 里如何發起 HTTP請求。我們通過向user視圖模型添加另一個方法來看看 fetch 模塊是如何工作的。
> ### 操作:在視圖模型里完成登錄
>
> ---
>
> 打開 `app/shared/view-models/user-view-model.js` 把下面的代碼直接粘貼到現有的`viewModel.register()` 方法上方:
>
> `viewModel.login = function() {`
>
> `return fetchModule.fetch(config.apiUrl + "oauth/token", {`
>
> `method: "POST", body: JSON.stringify({`
>
> `username: viewModel.get("email"),`
>
> `password: viewModel.get("password"),`
>
> `grant_type: "password"`
>
> `}),`
>
> `headers: {`
>
> `"Content-Type": "application/json"`
>
> `}`
>
> `})`
>
> `.then(handleErrors)`
>
> `.then(function(response) {`
>
> `return response.json();`
>
> `})`
>
> `.then(function(data) {`
>
> `config.token = data.Result.access_token;`
>
> `});`
>
> `};`
我們來分解下剛才你粘貼的代碼。
* 你用 fetch 模塊里的 `fetch()` 方法向存在 `shared/config.js` 里的 `apiUrl` 地址 POST 數據。 username,password 和 grant\_type 以json字符串的形式發送到這個端點 \(Telerik Backend Services [要求一個 grant\_type 參數](http://docs.telerik.com/platform/backend-services/rest/users/users-authenticate)參與登錄\) 。
* `fetch()` 方法返回一個`Promise`,它允許我們在異步登錄成功或失敗后執行代碼。你用這個功能做了三件事(那三個 `then()` 處理程序)。
* 首先,你用在 `user-view-model.js` 底部定義的 `handleErrors()` 方法處理 HTTP response 里的任何錯誤。(如果你想更多了解如何在`fetch()` 工作時處理 HTTP response里的錯誤,請看[這篇文章](http://tjvantoll.com/2015/09/13/fetch-and-errors/) )
* 然后,你調用 `Response` 對象的 `json()` 方法將返回數據轉換成JSON。
* 最后,你在 config 模塊里保存了一個用戶授權 token 的引用。后面的教程里,你將在隨后的 HTTP 請求里使用這個 token 。
> ### **提示**
>
> ---
>
> * [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 是 ECMAScript 6 的一部分。因為 Promises 實現了 NativeScript 使用的兩個JS引擎—android的V8 和IOS的 JavaScriptCore—Promises就可以在NativeScript apps里使用。
> * 為方便起見, NativeScript 把 `fetch()` 做成了可用的全局變量。這意味著你可以用 `fetch()` 替換 `Module.fetch()` 。全局的 `fetch()` 快捷方式將在本教程余下的部分使用。
搞定了這些代碼后,我們回到 `login.js` 來使用這個新方法。
> ### 操作:添加 **UserViewModel** 登錄后臺代碼
>
> ---
>
> 在 `login.js文件的頂部`, 添加`shared/view-models/user-view-model`的引用:
>
> `var UserViewModel = require("../../shared/view-models/user-view-model");`
>
> **刪除**下面五行代碼,因為你現在要用 `UserViewModel` 來取代你前面章節添加的`user` 。
>
> `// Remove these lines of code`
>
> `var Observable = require("data/observable").Observable;`
>
> `var user = new Observable({`
>
> `email: "user@domain.com",`
>
> `password: "password"`
>
> `});`
>
> 然后,在 `var UserViewModel = require(...)` 這一行后面加入下面這行代碼:
>
> `var user = new UserViewModel();`
>
> 最后,用下面的代碼替換`exports.signIn()` 方法:
>
> `exports.signIn = function() { user.login(); };`
>
> **注意:你可以隨時在 \*\***[sample-Groceries repo](https://github.com/NativeScript/sample-Groceries/tree/end)**\*\* 的“結束”分支查看已完成的代碼 。**
花點時間看下你現在的后臺代碼是多么干凈。后臺代碼實例化了一個視圖模型 \(`UserViewModel`\),然后在用戶點擊視圖里的 sign in 按鈕時調用它的 `signIn()` 方法。因為視圖模型是綁定了頁面的兩個文本域(記得`{{ email }}` 和`{{ password }}么?`),視圖模型就已經有了它進行登錄操作的這些數據。
如果你嘗試運行app,輸入你的賬戶憑據\(email和password\),你實際能登錄,但……你看不到任何反應。這是因為視圖模型不負責更新UI。相反,視圖模型返回一個 `Promise` 讓后臺代碼處理UI。(還記得 `fetch()` 會返回一個 `Promise` 么?)。我們來看下如何使用這個 `Promise` ,并在其中介紹一個新的 NativeScript 模塊。
## [**4.2: Dialog module**](http://docs.nativescript.org/tutorial/chapter-4#42-dialog-module)對話框模塊 {#4-2}
要使用視圖模型的 `login()` 方法返回的 `Promise` ,你需要處理兩個場景:登錄成功了做什么,登錄不成功做什么。
在雜貨店例子里,登錄成功時你就將把用戶導航到列表頁,這個稍后我們會搭建,它允許用戶在一個列表里面添加和移除雜貨。完成這個導航就會用到本教程此前用過的同一個frame 模塊。
棘手的情況是處理登錄失敗,為此你要使用 dialog 對話框模塊。你可以在app里用這個模塊顯示幾種形式的彈出界面,包括動作表,確認盒子,警告盒子和提示。這是個可高度自定義的模塊,它允許你控制alert的按鈕和文字,以及alert本身的消息傳遞。 dialog模塊和其他UI小部件的代碼都在 `node_modules/tns-core-modules/ui` 文件夾。我們來看如何在login頁面使用這個小部件。
> ### 操作:用 **dialog** 窗體處理
>
> ---
>
> 添加下面的代碼到 `login.js` 頂部以引入 dialog 模塊:
>
> `var dialogsModule = require("ui/dialogs");`
>
> 接著,像這樣重寫 `signIn()` 方法:
>
> `exports.signIn = function() {`
>
> `user.login()`
>
> `.catch(function(error) {`
>
> `console.log(error);`
>
> `dialogsModule.alert({`
>
> `message: "Unfortunately we could not find your account.",`
>
> `okButtonText: "OK"`
>
> `});`
>
> `return Promise.reject();`
>
> `})`
>
> `.then(function() {`
>
> `frameModule.topmost().navigate("views/list/list");`
>
> `});`
>
> `};`
這段代碼同時處理登錄成功或不成功。成功時,你調用 frame 模塊的`navigate()`方法 把用戶導航到 list 頁面(現在是空的)。失敗時,你使用 dialog 模塊向用戶顯示一個錯誤消息。試試輸入一些無效的憑據看看 dialog是什么樣子。

有了這些,登錄頁面就功能完備了。既然你的NativeScript 的用戶管理能運作了,我們就移步到用戶管理他們雜貨的列表頁面去。為此,你需要一個模塊來顯示列表里的項目,這也就是 **ListView** 模塊做的事。
> ### 提示:
>
> 本教程從現在往后,你會經常登錄,你會發現開發期間在app里硬編碼你的用戶憑據是很方便的。最簡單的辦法是向 `UserViewModel()` 構造器傳遞一組 email 地址和密碼,例如:
>
> `var user = new UserViewModel({ email: "username@domain.com", password: "password" });`
## [**4.3: ListView**](http://docs.nativescript.org/tutorial/chapter-4#43-listview)數據列表模塊 {#4-3}
ListView部件允許你在屏幕上顯示一些東西的列表,就像你想要顯示雜貨的列表。在雜貨列表和后臺API連接之前,我們來看看如何在屏幕上顯示一個硬編碼的列表。
> ### 操作:構造列表視圖
>
> ---
>
> `打開app/views/list/list.xml 并粘貼下面的代碼, 它創建了你的雜貨陳列的列表:`
>
> `<Page loaded="loaded">`
>
> `<GridLayout>`
>
> `<ListView items="{{ groceryList }}">`
>
> `<ListView.itemTemplate>`
>
> `<Label text="{{ name }}" horizontalAlignment="left" verticalAlignment="center"/>`
>
> `</ListView.itemTemplate>`
>
> `</ListView>`
>
> `</GridLayout>`
>
> `</Page>`
>
> ### 提示:
>
> ---
>
> 注意頁面將使用 `<GridLayout>` 在屏幕上布局UI組件。當你添加更多的UI組件,你會開始把屏幕切分成行和列,但現在你將只用 `<ListView>` 占據整個屏幕(這是 `<GridLayout>` 沒有屬性時的默認行為)。
之前說過,即便你在XML里使用 `<ListView>` , ListView 模塊也仍然是一個 NativeScript 模塊。你可以在 `node_modules/tns-core-modules/ui/list-view` 文件夾里找到它的實現。如果你想,你可以[像這個例子那樣](http://docs.nativescript.org/cookbook/ui/list-view),在后臺代碼文件中用純JavaScript 構建一個ListView。通常情況下在XML里使用 NativeScript UI組件更方便些,所以我們在本教程里通篇采用XML的用法。
注意 `<ListView.itemTemplate>` 的用法。這個標簽賦予你控制在列表里如何顯示 ListView的每個子項目的能力。現在你只用了一個簡單的 `<Label>` UI組件來顯示每個雜貨的 `{{ name }}` 。
如果你原樣運行這些代碼,你不會看到雜貨列表有任何東西。首先你需要建立一種方法來管理ListView模塊里面的數據,為此你需要一個新的 NativeScript 模塊:也就是 ObservableArray 。
## **[4.4: Working with arrays](http://docs.nativescript.org/tutorial/chapter-4#44-working-with-arrays)****\*\***\*\*_\*\_\*_\*\_\*_\*\_\*_\*\_\*_\*\_\*_\*\_\*_\*使\_\*用數組 {#4-4}
在本教程之前的部分你見過如何創建 observables 并如何用它們通過后臺代碼文件和視圖模型來聯接XML視圖。在本節你將要做同樣的事情,一個額外的聯接,它涉及到使這個 `items` 屬性生效。
`<ListView items="{{ groceryList }}">`
ListView組件的 `items` 屬性采用了一個數組,為了在視圖模型上創建這個數組, NativeScript 提供了一個特別的 ObservableArray 模塊。要看它是如何工作的,我們開始來構建 list 頁面的后臺代碼。
> #### **操作: Populate the list view**填充列表視圖
>
> ---
>
> 打開 `app/views/list/list.js` 并粘貼下面的代碼:
>
> `var dialogsModule = require("ui/dialogs");`
>
> `var Observable = require("data/observable").Observable;`
>
> `var ObservableArray = require("data/observable-array").ObservableArray;`
>
> `var page;`
>
> `var pageData = new Observable({`
>
> `groceryList: new ObservableArray([`
>
> `{ name: "eggs" },`
>
> `{ name: "bread" },`
>
> `{ name: "cereal" }`
>
> `])`
>
> `});`
>
> `exports.loaded = function(args) {`
>
> `page = args.object;`
>
> `page.bindingContext = pageData;`
>
> `};`
這里,你將創建一個新的 Observable 對象名為 `pageData` ,你在 `load()` 方法里將它設置為頁面的 `bindingContext` 。 在Observable 內部,你單獨設置了一個 `"groceryList"` 屬性作為 ObservableArray 類的一個新實例。注意 `"groceryList"` 屬性如何與 `<ListView items="{{ groceryList }}">` 對應,也注意數組里面的每個 `"name"` 屬性如何與 `<Label text="{{ name }}">對應` 。如果運行app你會看到list界面顯示出硬編碼數據:

既然我們在屏幕上有了項目,我們來看下這個列表如何與后臺聯接以替換掉硬編碼數據。為此你要用一個視圖模型來改變list頁面,就像我們在login頁面做的那樣。
本頁面初始的視圖模型已經在位于 `app/shared/view-models/grocery-list-view-model.js` 的文件里了,它包含的代碼看起來很像你已有的 `list.js` 文件里的。
`var config = require("../../shared/config");`
`var fetchModule = require("fetch");`
`var ObservableArray = require("data/observable-array").ObservableArray;`
`function GroceryListViewModel(items) {`
`var viewModel = new ObservableArray(items);`
`return viewModel;`
`}`
`module.exports = GroceryListViewModel;`
我們來擴展它以把這個視圖模型和后端聯接。
> ### **操作: 從后端填充列表視圖**
>
> ---
>
> 你要從使用 `GroceryListViewModel` 改變 `list.js` 開始。首先, `require()` 引入 `GroceryListViewModel` 才能使用它:
>
> `var GroceryListViewModel = require("../../shared/view-models/grocery-list-view-model");`
>
> 接著,**移除**現有的 `var pageData` 定義:
>
> `// Remove these seven lines of code`
>
> `var pageData = new Observable({`
>
> `groceryList: new ObservableArray([`
>
> `{ name: "eggs" },`
>
> `{ name: "bread" },`
>
> `{ name: "cereal" }`
>
> `])`
>
> `});`
>
> 在相同的位置增加下面的代碼:
>
> `var groceryList = new GroceryListViewModel([]);`
>
> `var pageData = new Observable({`
>
> `groceryList: groceryList`
>
> `});`
>
> 最后,用下面的方法代碼替換現有的 `exports.loaded()` 方法,它在視圖模型層調用了兩個新的方法—— `empty()` 和 `load()` 。
>
> `exports.loaded = function(args) {`
>
> `page = args.object;`
>
> `page.bindingContext = pageData;`
>
> `groceryList.empty();`
>
> `groceryList.load();`
>
> `};`
>
> 這段代碼里, `groceryList` 指向的是 grocery list模型,并且 `empty()` 方法清空了list,然后調用視圖模型的 `load()` 方法從后端重新加載數據。
>
> 讓這一切起作用的最后一段代碼實際上是在視圖模型里實現 `empty()` 和`load()` 方法。打開 `app/shared/view-models/grocery-list-view-model.js` ,在 `var viewModel` 定義和 `return viewModel` 語句之間粘貼下面的代碼:
>
> `viewModel.load = function() {`
>
> `return fetch(config.apiUrl + "Groceries", {`
>
> `headers: {`
>
> `"Authorization": "Bearer " + config.token`
>
> `}`
>
> `})`
>
> `.then(handleErrors)`
>
> `.then(function(response) {`
>
> `return response.json();`
>
> `}).then(function(data) {`
>
> `data.Result.forEach(function(grocery) {`
>
> `viewModel.push({`
>
> `name: grocery.Name,`
>
> `id: grocery.Id`
>
> `});`
>
> `});`
>
> `});`
>
> `};`
>
> `viewModel.empty = function() {`
>
> `while (viewModel.length) {`
>
> `viewModel.pop();`
>
> `}`
>
> `};`
上述代碼發起了一個有點眼熟的HTTP對話,因為它利用了你在前面部分用過的同一個 fetch 模塊,該 fetch 模塊 的第一個 `then()` 處理器 檢查 HTTP 的錯誤,第二個 `then()` 處理器 把數據從 response 轉換為 JSON 格式,第三個處理器把每個雜物項目從 response 推送到 ObservableArray 里面。
如果你運行app并使用email地址“ user@nativescript.org ”和密碼“ password ”登錄,你會看到像下面這樣的雜物列表:

這里最酷的是你不用寫代碼。注意這里沒有必要刷新UI,或者手動獲取 ListView UI組件——所有視圖模型做的只是把JSON response 推送給 ObservableArray ,UI就自己處理了。
我們來看看你如何能在此基礎上構建并允許用戶直接從app添加到他們的雜物列表。
## [**4.5: GridLayout**](http://docs.nativescript.org/tutorial/chapter-4#45-gridlayout)格子布局 {#4-5}
你已經知道如何添加項目到一個 `<ListView>` 了,因為你剛剛在上一節這樣做了——需要做的就是調用 ObservableArray 模塊的 `push()` 方法。
為了讓用戶能管理他們的雜貨列表,你將不得不在屏幕上放置更多的UI組件,為此你將需要用`<GridLayout>` UI組件把屏幕分割成行和列。
> ### **操作: 實現添加雜貨到列表**
>
> ---
>
> 打開 `app/views/list/list.xml` 用下面的代碼更改`<GridLayout>` 標簽:
>
> `<GridLayout rows="auto, *" columns="2*, *">`
>
> 這里的`rows`屬性把屏幕切分成兩行,第一行參照它的子組件高度自動設置,另一行包含\*,或者說屏幕剩余的高度。 `columns` 屬性把屏幕分成兩列,第一列取得屏幕2\/3寬度,第二列取得剩下三分之一。
>
> 然后,為了給給用戶添加雜貨到列表的路徑,在頁面添加一個文本域和一個按鈕。把下面兩行代碼添加到緊接著初始 `<GridLayout>`標簽下面:
>
> `<TextField id="grocery" text="{{ grocery }}" hint="Enter a grocery item" row="0" col="0" />`
>
> `<Button text="Add" tap="add" row="0" col="1" />`
>
> 該`TextField`** **有一個id屬性 `"grocery"`,并綁定到頁面的 binding context 的 `{{ grocery }}` 屬性。按鈕的 `tap` 行為指向一個 `add()` 方法,你稍后會在后臺代碼文件添加它。
>
> 但這里最需要注意的是 `row` 和`col` 屬性的用法。這些屬性是0-基數的,所以 `TextField` 的 `row="0" col="0"` 屬性把它放在了第一行第一列,而 `Button` 的 `row="0" col="1"` 屬性把它放在了第一行第二列。
>
> 最后,在第二行里用下面的代碼替換 `<ListView>` 標簽,讓它跨占兩列,也給它一個id屬性(你后面會用到的)。
>
> `<ListView items="{{ groceryList }}" id="groceryList" row="1" colSpan="2">`
>
> ### _以上是xml代碼更改,以下是后臺代碼更改_
>
> ---
>
> 現在你只需對后臺代碼文件做一些必要的更改來支持XML的更改。打開`list.js,`首先添加一個新的 `"grocery"` 屬性到 `pageData`Observable 。 `pageData` 定義看起來應是這樣的:
>
> `var pageData = new Observable({`
>
> `groceryList: groceryList,`
>
> `grocery: ""`
>
> `});`
>
> 然后,你需要添加一個 `add()` 方法處理按鈕點擊事件。粘貼下面的代碼到 `list.js` 底部:
>
> `exports.add = function() {`
>
> `// Check for empty submissions`
>
> `if (pageData.get("grocery").trim() === "") {`
>
> `dialogsModule.alert({`
>
> `message: "Enter a grocery item",`
>
> `okButtonText: "OK"`
>
> `});`
>
> `return;`
>
> `}`
>
> `// Dismiss the keyboard`
>
> `page.getViewById("grocery").dismissSoftInput();`
>
> `groceryList.add(pageData.get("grocery")) .catch(function() {`
>
> `dialogsModule.alert({`
>
> `message: "An error occurred while adding an item to your list.",`
>
> `okButtonText: "OK"`
>
> `});`
>
> `});`
>
> `// Empty the input field`
>
> `pageData.set("grocery", "");`
>
> `};`
>
> 在這個方法里,你首先確定用戶沒有未輸入雜貨就提交。如果用戶有輸入,收起輸入鍵盤,就從頁面的綁定上下文(這里綁定到那個新的 `<TextField>` )得到 `"grocery"` 屬性,然后把這個值傳給視圖模型的 `add()` 方法。
>
> 最后,定義這個 `add()` 方法。為此,打開 `app/shared/view-models/grocery-list-view-model.js` 并粘貼下面的方法到 `empty()` 方法的下面,但要在 `return viewModel` 語句之前:
>
> `viewModel.add = function(grocery) {`
>
> `return fetch(config.apiUrl + "Groceries", {`
>
> `method: "POST",`
>
> `body: JSON.stringify({`
>
> `Name: grocery`
>
> `}),`
>
> `headers: {`
>
> `"Authorization": "Bearer " + config.token,`
>
> `"Content-Type": "application/json"`
>
> `}`
>
> `})`
>
> `.then(handleErrors)`
>
> `.then(function(response) { return response.json(); })`
>
> `.then(function(data) {`
>
> `viewModel.push({ name: grocery, id: data.Result.Id });`
>
> `});`
>
> `};`
搞定后回到你的app,你會發現你能添加雜貨項目了且能立即出現在列表里——并且,所有這些完全由后臺服務驅動。夠屌吧?

我們來看下如何通過一個顯示活動的指示器來優化這個頁面。
## **[4.6: ActivityIndicator](http://docs.nativescript.org/tutorial/chapter-4#46-activityindicator)**活動指示器 {#4-6}
目前當你首次訪問列表頁時,雜貨出現前有一些延遲。這個延遲可能會困擾新用戶,他可能覺得app卡住了,而不是正在從后端檢索數據。
在 NativeScript app里,當你的app忙于運行的時候,你可以在你的UI里使用 ActivityIndicator 模塊來顯示一個旋轉圖標。該活動指示器是個相當簡單的UI組件,因為它主要使用一個屬性—— `busy` 。當一個活動指示器的 `busy` 屬性被設置為`true`時活動指示器顯示,當 `busy` 屬性被設置為false的時候不顯示。我們通過添加一個活動指示器到列表頁面來看下這個模塊是如何工作的。
> #### **操作:添加一個活動指示器**
>
> ---
>
> 打開 `app/views/list/list.xml` 吧下面的元素直接添加到 結束標簽`</GridLayout>`前面。
>
> `<ActivityIndicator busy="{{ isLoading }}" rowSpan="2" colSpan="2" />`
>
> 然后, 在 `app/views/list/list.js`里面, 用下面的四行代碼 替換現有的 `groceryList.load()` 也就是`loaded()`:
>
> `pageData.set("isLoading", true);`
>
> `groceryList.load().then(function() {`
>
> `pageData.set("isLoading", false);`
>
> `});`
上面的代碼里,你添加了一個新的 `"isLoading"` 標識到列表頁的 Observable ,然后將活動指示器的 `busy`屬性與它的值綁定。 在頁面的 `loaded()` 方法里,你設定 `"isLoading"` 標識的初始值為 `true` ,這將顯示活動指示器。當雜貨列表完成加載,你反轉 `"isLoading"` 標識回到 `false` ,活動指示器隱藏。
你通過設置活動指示器的 `rowSpan` 和 `colSpan` 屬性控制它的顯示位置。本例的`rowSpan="2" colSpan="2"` 讓活動指示器占據了其父組件 GridLayout 的兩行和兩列。下面試這個新的活動指示器的表現:

列表頁現在看起來親和多了,但我們可以用一個更加強大的模塊來提升用戶體驗:**動畫模塊**。
## **[4.7: Animations](http://docs.nativescript.org/tutorial/chapter-4#47-animations)**動畫 {#4-7}
運行的穩健和高性能的動畫的能力是人們選擇制作原生移動app的最主要原因之一, 而NativeScript 讓運行動畫變簡單了。 NativeScript 動畫模塊提供一個 [JavaScript APIs套件](http://docs.nativescript.org/ui/animation) 讓你用運行很多種動畫來處理屏幕上的元素,包括如下:
* [Opacity不透明度](http://docs.nativescript.org/ui/animation#opacity)
* [Background Color背景色](http://docs.nativescript.org/ui/animation#background-color)
* [Translations移動](http://docs.nativescript.org/ui/animation#translate)
* [Scaling放大縮小](http://docs.nativescript.org/ui/animation#scale)
* [Rotating旋轉](http://docs.nativescript.org/ui/animation#rotate)
在我們的列表頁面你將使用一個[不透明度動畫](http://docs.nativescript.org/ui/animation#opacity)在數據加載后來淡入顯示你的雜貨列表。我們來添加代碼然后探討下它是如何工作的。
> #### **操作: 添加一個動畫**
>
> ---
>
> 打開 `app/views/list/list.css` 添加一個 `opacity: 0` 規則到現有的 `ListView` 選擇器。 全套規則應該是這樣:
>
> `ListView {`
>
> `margin: 5;`
>
> `opacity: 0;`
>
> `}`
>
> 然后,在 `app/views/list/list.js`里,用下面的代碼替換現有的 `exports.loaded()` 方法,它新做了兩件事情:獲取一個 `<ListView>`\(`page.getViewById("groceryList")`\) 的引用,然后在 `groceryList.load()` 調用完成后調用 `<ListView>` 這個元素的 `animate()` 方法。
>
> `exports.loaded = function(args) {`
>
> `page = args.object;`
>
> `var listView = page.getViewById("groceryList");`
>
> `page.bindingContext = pageData;`
>
> `groceryList.empty();`
>
> `pageData.set("isLoading", true);`
>
> `groceryList.load().then(function() {`
>
> `pageData.set("isLoading", false);`
>
> `listView.animate({`
>
> `opacity: 1,`
>
> `duration: 1000`
>
> `});`
>
> `});`
>
> `};`
一些新的狀況在上面的代碼里發生了。
首先,在CSS里,你把雜貨列表 `<ListView>` 的 `opacity` 定義為0。這使得雜貨列表在頁面加載的時候完全隱藏。然后,JavaScript 里, `groceryList.load()` 調用完成之后,你調用了列表視圖的 `animate()` 方法。它把該元素的 `opacity` 在一秒內從0(完全隱藏)變成了1(完全可見)。
> **注意**: **animation 方法的 **`duration`** 屬性采用毫秒計數.。因此, **`1000`** 就是1秒。**
The result of this code is a nice fade-in animation:
這段代碼的結果就是一個不錯的淡入動畫:

動畫模塊玩起來很有意思,并且使用簡單。你需要做的就是用 `getViewById()` 獲取元素的引用,然后調用這個元素的 `animate` 方法。或許你想花幾分鐘通篇了解下我們的 [animation samples](http://docs.nativescript.org/ui/animation#examples) 并在你的雜貨店里嘗試里面的一些動畫。
既然你完成了登錄、注冊和列表頁面,我們來提升app的功能把它作為一個雜貨列表的管理工具。下一章你將添加email驗證、社交分享等功能。你會使用 NativeScript的最有用的內動來完成它:npm模塊。
> **提示:**
>
> ---
>
> 你安裝 NativeScript 后盒子里冒出的模塊太多了,以至于我們不能在本教程里一一涵蓋——包括 [location service定位服務](http://docs.nativescript.org/cookbook/location), [file-system helper文件系統助手](http://docs.nativescript.org/cookbook/file-system), [timer module時間模塊](http://docs.nativescript.org/cookbook/timer) ,[camera module相機模塊](http://docs.nativescript.org/cookbook/camera),[color module顏色模塊](http://docs.nativescript.org/cookbook/color),更多更多。 一定要細讀 官方文檔的“Modules API” ,或者全面看看 `node_modules/tns-core-modules` 以了解所有哪些可以使用。