2017/12/1

使用遠端的 AVD 進行測試

測試上遇到的困擾

測試一直是軟體開發中重要的一環,在「軟體測試經驗分享」中,提到了一些測試相關的概念。Android 的開發裡,AVD 則是測試不可或缺的一部份。透過 AVD 可以不用準備大量的實體設備,就可以讓所開發的 App 在不同的環境中檢視其運作的情況。

使用 Notebook 進行 Android 開發的人,通常都會遇到一個困擾是:Notebook 的磁碟空間大多不是很充裕。在開發 Android App 時,常常要使用多個 AVD,來測試 App 在不同的環境下都可以正常地運作,畢竟 Android 的平台破碎化已經是一種特色了。

然而在空間有限的情況下,建一個 AVD 就要上 Giga 的空間,這還不包含不同版本的 Image 檔。每一個版本還有分 32bit、64 bit,有 Google API、沒有 Google API,再加上 TV 和 Wear 這二種設備種類。林林總總的 Image 加起來,配上 AVD 可能要產生出不同畫面尺寸的組合,對於有限的磁碟空間來說更是捉襟見肘。

如果要擴充 Notebook 的儲存空間,除了價格上的因素以外,往往還會受限於 Notebook 硬體架構上的侷限性。就這個缺點來說,PC 相對是一個比較好的選項。但是改為使用 PC 開發,就會失去了原本 Notebook 俱備的機動性。想要二者優點兼俱,替代性的選項是在二台機器間進行 Source Code 同步,需要機動性就在 Notebook 上開發,需要空間時就轉移到 PC 上開發。

這個選項對大型公司而言,是一個再直接不過的選項,因為公司內通常都會有一定程度的版本控管系統在協助,檔案移轉的負擔比較小,甚至更豪華的有專用 Lab 環境。反過來說,不在這樣體制內的人,只能苦命地手動將 Source Code 反覆傳來傳去,費工又容易出錯。

對於這樣的族群來說,需要將選項的內容再進化,也就是開發仍在 Notebook 上保持機動性,同時選擇關鍵的環境來建立 AVD,做基本的確認。而 PC 上則是可以廣泛地建立不同選項的 AVD,要進行測試時就將二個環境結合起來,將 Notebook 建置好的結果傳送到 PC 上執行測試。

所以現在的問題是,這樣的要求做得到嗎?要怎麼做?

可行的方案

首先,先來了解負責管理受測裝置的工具 - ADB 的運作原理,這個工具包含了三個部份 Client、Daemon、Server:
  • Client: 負責接收我們輸入的指令,並且傳送給所連接的 Server,一般就是我們在 Command-line 使用的 adb 指令。
  • Daemon: 駐紮在行動裝置上,可以是實體設備或是 AVD,負責在裝置上執行由 Server 傳來的指令。
  • Server: 在電腦上執行,負責管理 Client 與 Daemon 之間的訊息傳遞。


從上圖來看,可行的方案有三個:第一個是透過 Server 轉送訊息給 Daemon,第二個是讓 Client 連接到遠端的 Server,第三個方案則是讓 Server 連到遠端的 Daemon。

第一個方案是使用 adb forward 的指令,讓 Daemon 可以收到所處 PC 向 127.0.0.1 發出的訊息,並轉給 AVD 指定的 Port。這個動作最直接的效果,是讓所在的 PC 可以不用透過 Console,直接與 AVD 中的 Service 進行溝通。但似乎與跨機器無關,所以先略過。

第二個方案要達成不難,在 adb 的指令中有參數分別是 -H-P 可以指定 Server 的 Address 及 Port。例如以下的指令可以列出 192.168.1.1 電腦上,ADB Server 所管理的裝置清單。

adb devices -H 192.168.1.1 -P 5038

但有一個很關鍵的問題是,如何設定讓 Android Studio 使用遠端的 ADB Server 來進行部署及測試?IDE 和 Gradle 我都沒有找到答案,也許是這樣的選項還不存在吧!會有這個結果,最主要的問題應該是卡在如何讓遠端的 ADB Server 拿到建置好的結果,以傳送進 AVD 內。

不過,倒是可以使用以下示範的指令來啟動遠端ADB Server 的測試,只不過連部署的工作都要在建置完成後,用 adb 自己動手。

adb shell am instrument -w <test_package_name>/<runner_class> -H 192.168.1.1 -P 5038

方案三,也是可以用adb 的指令來達成,以下的指令可以讓 ABD Server 連接到 192.168.1.1 電腦上的 AVD。

adb connect 192.168.1.1:3333

連上之後,就可以在 IDE 的「Select Deployment Target」的視窗中,看到遠端的 AVD 被列在「Connected Devices」清單中。接下來的動作就跟過往一樣,選擇遠端 AVD 的項目後,按下【OK】,等建置完成就可以看到 App 出現在遠端 AVD 的畫面裡。

這個方案比第二個更具優勢的是,可以直接在 IDE 或是 Gradle 中就完成部署及測試,維持原本的開發經驗一氣呵成,不用再另外做設定。

需注意的技術細節

在方案二或方案三的指令中,如果直接進行連線,應該會遇到問題、無法完成連線。主要的原因可以看一下圖示:


ADB Server 聆聽的 Address 是 127.0.0.1,而外部傳入的 Port 是在真實的 IP 上,也就是說訊息並沒有傳到 ADB Server 所在的 Port 上。為了要解決這樣的情況就必須要把真實 IP 接到的訊息 Forward 到 127.0.0.1 所在的 Port。

以 Windows 為例,內建有 netsh 指令來進行這項工作。以下的指令就是把真實 IP 在 5038 上收到的訊息,轉送到 ADB 預設的 5037 Port 上:

netsh interface port proxy add v4tov4 listenport=5038 connectaddress=127.0.0.1 connectport=5037

這裡有個小細節要注意,在設定 ADB Server 的聆聽 Port 轉送時,不能使用同一個 5037,會導致 ADB Server 無法啟動。

同時,AVD 的 Port 是成對配置的,ADB 管理的範圍是由 5554 到 5585,每二個號碼分配給一個 AVD,偶數的號碼是給 Console 連線用,奇數才是保留給 ADB。第一個 AVD 分配到的會是 5554,而 ADB 使用的則為 5555。以下圖的 AVD 為例,顯示的是 5558,但要進行遠端連線的 Port 應該加一號為 5559。



AVD 的 Port 也不要佔用到 5554 到 5585 的範圍,否則 ADB 會認為 Port 被分配走,而跳過改取下一組。

這個設定是有持績性的,不會因為重新開機而消失。而執行這個指令需要有 Administrator 的權限,所以可以一次就設定好清單,畢竟範圍也就只有從 5555 到 5585。如此一來,不論 AVD 被分配到哪一個 Port 上,都可以被遠端連接上。

所能達到的效果

最後總結一下以這種方式進行開發的好處:
  • 節省開發環境的磁碟空間,並且可以更彈性地選擇測試設備的規格,不用被開發的機器所限制。
  • 有效利用閒置資源、增加工作效率。用 Notebook 開發的人,另外擁有 PC 是很常見的情況,只是很有可能在開發時是閒置的。而開發時最耗資源的都是測試,所以如果可以把耗資源的部份轉移到效能相對較好的 PC 上,可以有效地提高效率。
  • 減少空轉與等待的情況。延續上一點,耗資源的工作被轉出去,開發的機器就可以接續進行開發的工作,或是放鬆休閒一下,上上網、看個影片,等待測試完成。

額外的異常狀況

在嘗試以上解決方案的過程中,遇到了一些異常的情況,順便記錄一下解決的方法。

首先,用 Windows 做為遠端測試設備時,除了要設定 Port Forward 之外,還要確認 Firewall 是否有阻擋訊息的交換。如果 Firewall 是在開啟的狀態之下,有很大的機會訊息是無法傳遞的。此時,就要調整 Firewall 的設定,增加適合的允許規則。

在使用最新版的 Android SDK Tools 時,與先前的版本不同的是,不再提供像 Android Studio 所附的 GUI 工具,完全是由 Command-line 執行工作。不過,也還好,問題不大,就是打字多了點。

有問題的是 avdmanager 在參數被大幅減化之下,產生出來的 AVD 似乎與 Android Studio 建立出來的有差距。再來是 sdkmanager 可下載的 package 中,似乎也沒看到 AVD Skin 這個選項。結果用 avdmanagerdevice 參數做出來的 AVD,縮放都不太正常,畫面大到不合理,只能看到 AVD Desktop 的左上角。而用來啟動 AVD 的 emulator 看似可以調整這個問題的參數,在新版也早就被廢棄了。

為了這個問題,查了好久,試了很多參數,都沒有辦法解決。根據網路上的說法,似乎是個 Bug,而且有一段時間了、歷經了好幾個版本都沒有修正,真是好樣的,Google。

原本想要放棄,改為安裝 Android Studio 算了。只是突然靈機一閃,把其他機器產生好的 AVD 移過來,有缺 Skins 這個目錄的也要一併把 Skins 移到 SDK 下。調整一下 ini 裡路徑的設定,居然就可以正常地顯示了,目前看來堪用中。








2017/11/14

FluxJava 新增 RxJava2 的支援功能

FluxJava 最初的設計就是以 Add-on 的方式來提供對於 RxJava 的支援,所以這次增加 RxJava2 的部份也依照相同的模式,在 Project 中加上了 fluxjava-rx2 的 Module。新的 Module 功能上與 fluxjava-rx 大致上相同,只是原本以 RxJava 規格運作的部份,改為使用 RxJava2。

由於 RxJava 與 RxJava2 不太有機會共存在同一個 Module 裡,所以 fluxjava-rx2 沿用了 fluxjava-rx 的 Package 名稱,在使用上這二個 Add-on 必須要擇一引用。不過也帶來了一個額外的好處,如果想要由 fluxjava-rx 昇級到 fluxjava-rx2 時,只要修改成 RxJava2 的呼叫規格,不用再特別調整 Import 的內容。

配合 RxJava2 的更新,按照慣例增加了一篇文章「FluxJava 與 RxJava2 結合的使用示範」做為使用上的參考。

在 fluxjava-rx2 中與 fluxjava-rx 最大的差異,主要是因應 RxJava2 把原本的 Observable 分成了有背壓版本的 Flowable 與沒有背壓版本的 Observable。因此 RxBus 與 RxStore 中都分別再提供了 toFlowable 的 Method 來取得 Flowable,不過 Observable 本來就可以再轉換為 Flowable,此處的功能只是為了增加便利性、簡化程式碼之用。

同時,利用這篇文章補充說明一下一些使用上的技術細節。在 RxStore 中使用的是沒有背壓版本的 Observable 來接收外部傳來的訊息,只是接收到之後就會被分派到不同的 Thread 上去處理後續的工作,所以在這個部份是不大有機會遇上 MissingBackpressureException 問題的。但是,這並不代表背壓所造成的情況就不存在了,只是瓶頸移到了 ThreadPool 的承受能力或是執行的環境可以產生 Thread 的數量上。

就算是使用有背壓功能的 Flowable,也不代表就可以高枕無憂了。說穿了,背壓本身不是什麼神奇的黑科技,原理上只是在上下游中間加了個水池,讓下游有喘息的空間。水池畢竟還是有一定的物理限制,沒有控制好,依舊會讓水池承載不下而出現錯誤。

因此,不論使用哪一種方法,前端發送的數量仍然應該要被謹慎地控制,避免海量的訊息把接收端給淹沒了。像是把 RecyclerView 滾動時產生位移的 Event 毫無選擇地往 RxStore 送。

在發送端就要篩選訊息,除了要減少 MissingBackpressureException 可能會出現的機會外,還有一個目的是要節省執行成本。當訊息數量大到無法處理時,能做的只有挑選值得處理的部份來進行。當挑選的工作被移到發送後,RxJava 的傳送機制表面上看起來就是簡單地轉了一手,但是如果去追蹤其程式碼,就可以發現其實底下做了不少的工作。而每一次傳送都要執行這些內容,可以想見在數量到達一定的程度之後,就會顯現出可觀的效能差距。

由於 Observable 在定義上所形成的限制使然,同一個發送源無法把的訊息分配至不同的 Thread 上送出。RxStore 為了要讓每個資料處理要求可以獨立、同步地進行,所以才會在接收到訊息後,以不同的 Thread 進行後續的工作。

在 RxStore 裡提供了一個 getExecutor Method,可以使用 ThreadPool 來做為背壓的替代方案,但是並沒有像背壓一樣有可以控制上游的功能。在實作 getExecutor 時要注意,不要直接在回傳時 New 一個 ThreadPool 的 Instance。因為 getExecutor 是在每一次收到訊息後呼叫一次。以上的做法,也等同於每一次收到訊息就拿到一個 ThreadPool 的 Instance,每一個 ThreadPool 都只會產生一個 Thread,這就失去了使用 ThreadPool 的用意。

最容易出現以上問題的情況是使用 Executors 來取得 ThreadPool,一般的情況下很容易忽略 Executors 其實是每次呼叫就產生一個 Instance。所以當以 return Executors.newFixedThreadPool(poolSize); 的方式回傳 getExecutor 時,一開始也許顯示不出問題而被略過,但是一但訊息爆量後,就會因為產生 Thread 的數量到達上限而中止運作。

以上,是這次補充的內容,讓使用 FluxJava 的朋友做為參考。






2017/11/13

FluxJava 與 RxJava2 結合的使用示範

這篇是「FluxJava: 給 Java 使用的 Flux 函式庫」的延續,會透過建構一個示範的 Todo App 的過程來說明如何使用 FluxJava,所有示範的原始碼都可以在 Github 上找到。

大部份的內容其實都已經寫在另一篇中,但考量到有一些性急的朋友對於要開另外一篇文章,來看與想找的主題無關的內容會感到不耐,所以在這篇文章中還是會呈現完整的步驟及相關的內容。

Flux 簡介

為了方便不熟悉 Flux 的讀者,一開始會先簡短地說明這個架構。以下是借用 Facebook 在 Flux 官網上的原圖:


從圖上可以看到所有箭頭都是單向的,且形成一個封閉的循環。這一個循環代表的是資料在 Flux 架構中流動的過程,整個流程以 Dispatcher 為集散中心。

Action 大多是由與使用者互動的畫面元件所發起,在透過某種 Creator 的方法產生之後被送入 Dispatcher。此時,已經有跟 Dispatcher 註冊過的 Store 都會被呼叫,並且經由預先在 Store 上定義好的 Method 接收到 Action。Store 會在這個接收到 Action 的 Method 中,將資料的狀態依照 Action 的內容來進行調整。

前端的畫面元件或是負責控制畫面的元件,會收到由 Store 在處理完資料後所送出的異動事件,異動的事件就是用來代表在資料層的資料狀態已經不一樣了。這些前端元件在自行訂義的事件處理方法中聆聽、截收這些事件,並且在事件收到後由 Store 獲取最新的資料狀態。最後前端元件觸發自己內部的更新畫面程序,讓畫面上所有階層子元件都能夠反應新的資料狀態,如此完成一個資料流動的循環。

以上是一個很簡單的說明,如果需要了解更進一步的內容,可以自行上網搜尋,現在網路上應該已經有為數不少的文章可以參考。

以 BDD 來做為需求的開端

為了能夠更清楚的說明程式碼的細節,所以文章中會依照 BDD 的概念來逐步解說程式碼。所以首先是要先列出需求:
  • 顯示 Todo 清單
  • 在不同使用者間切換 Todo 清單
  • 新增 Todo
  • 關閉/重啟 Todo
接著下來就要把需求轉更明確的敘述內容,因為只是示範,所列僅做出一個 Story 做為目標:
Story: Manage todo items

Narrative:
As a user
I want to manage todo items
So I can track something to be done


Scenario 1: Add a todo
When I tap the add menu on main activity
Then I see the add todo screen

When I input todo detail and press ADD button
Then I see a new entry in list


Scenario 2: Add a todo, but cancel the action
When I tap the add menu on main activity
 And I press cancel button
Then Nothing happen


Scenario 3: Switch user
When I select a different user
Then I see the list changed


Scenario 4: Mark a todo as done
When I mark a todo as done
Then I see the todo has a check mark and strike through on title
也因為只是示範用,Story 的內容並沒有很嚴謹,而示範所使用的文字雖然是英文,但在實際的案例上用自己習慣的文字即可。

規劃測試的方略

既然是採用 BDD 來做示範,當然在程式撰寫的過程中會希望能夠有適切的工具的輔助,畢竟工欲善其事、必先利其器。所以在撰寫測試程式碼時,不使用 Android 範本中的 JUnit,改為使用在「使用 Android Studio 開發 Web 程式 - 測試」提到的 Spock Framework,並且全部以 Groovy 來撰寫。因為 Spock Framework 內建了支援 BDD 的功能,把之前做好的 Story 轉成測試程式碼的工作也會簡化很多。

接下來要決定如何在 Android 專案中定位與分配測試程式碼。Espresso 是 Android 開發環境中內建的,使用 Espresso 來開發測試程式還是有一定的必要性。只不過 Espresso 必須要跑在實機或模擬的環境上,執行效率問題是無法被忽視的一個因素,而且 androidTest 下的程式碼其執行結果也沒有辦法在 Android Studio 中檢視 Code Coverage 的狀態,所以用 Espresso 撰寫的測試程式並不適合用來做為 Unit Test。

再加上新版的 Android Studio 提供了錄製測試步驟的功能,最後會被轉成 Espresso 的程式碼。所以看起來 Espresso 比較適合用來做為開發程流後段的一些測試工作,像是 UAT、壓力測試、穩定度測試。依據這樣的定位,之前寫好的 Story 會在 Espresso 上轉成測試程式碼,來驗證程式的功能是否有達到 Story 描述的內容。

單元測試的部份沒有疑問地應該是寫在 Android 專案範本所提供的 test 的路徑之下,要解決的是 Android 元件如何在 JVM 的環境中執行測試。大部份人目前的選擇應該都會是 Robolectric,只不過測試程式碼要使用 Spock 來開發,所以這二個套件必須要做個整合。RoboSpock 就是提供此一解決方案的套件,可以讓 Robolectric 在基於 Spock 所開發的 Class 中可以被直接使用。

使用 Robolectric 雖然能夠對 Android 元件在 JVM 中進行測試,但畢竟這類的元件相互之間的藕合性還是有點高,尤其是提供畫面的元件。所以這個部分在歸類上我定位成 Integration Test,但在資料的供給上,拜 Flux 架構之賜,可以依照情境來進行代換,只測試 Android 元件與元件之間的整合度,這個部份在接下來的內容會進行說明。附帶一提,有關測試上的一些想法我有寫成一篇文章,可以參考:「軟體測試經驗分享」。

以下列出本次使用的測試套件清單:
  • Groovy
  • Spock Framework 
  • RoboSpock
  • Espresso
設定 Spock 與 Espresso、Robolectric 時會有一些細節需要注意,相關的說明請參考「設定 build.gradle 來用 Spock 對 Android 元件進行測試」。最後的 build.gradle 設定結果,可以在 Github 上的檔案內容中看到。

建立畫面配置

在產生完 Android 專案空殼後,首先修改 MainActivity 的內容。在 MainActivity 畫面中加上 RecyclerView 及 Spinner 來顯示 Todo 清單以及提供切換使用者的功能。Layout 的配置顯示如下:

開發 UAT

原本範本中預設產生的 androidTest/java 的路徑可以刪除,另外要在 androidTest 之下增加一個 groovy 的目錄,如果 build.gradle 有設定正確,在 groovy 目錄上應該會出現代表測試程式碼的底色。因為目前只有一個 Story 所以在 groovy 路徑下配對的 Package 中增加一個 ManageTodoStory.groovy 的檔案。

在這裡就可以顯現 Spock 所帶來的優勢,把之前的 Story 內容轉成以下的程式碼,與原本的 Story 比對並沒有太大的差距。

如果 Story 是用中文寫成的,以上的套用方式還是適用的。有關 Spock 的使用方式在這裡就不詳細地說明,各位可以自行上網搜尋,或是參考我之前寫的這一篇這一篇有關 Spock 的文章。接著就是把程式碼填入,完成後的內容如下所示:

以上程式碼中使用到的資源當然是要在撰寫之前就事件準備好,否則出現錯誤的訊息。完成後先執行一次測試,當然結果都是失敗的,接下來就可以依照需求來逐項開發功能。

了解 RxBus

除了原本 IFluxBus 所定義的 Method 以外,為了善用 RxJava 所帶來的優勢,RxBus 提供一個 toObservable 的 Method,利用這個 Method 所傳回的 Object,可以進行所有 RxJava 所提供的功能。

同時,在向 Observable 訂閱時會取得 Subscription,RxBus 可以協助管理 Subscription。在 Subscriber 向 RxBus 取消註冊時,一併更新所屬的 Subscription 狀態,以避免持續地收到通知。

如果是透過 registerunregister 來註冊 Subscriber,則不需要特別處理 Subscription 的問題。但如果是由先前所提到 toObservable 的 Method 來訂閱,則另外要呼叫 addSubscriptionremoveSubscription 來將 Subscription 列入與移除 RxBus 的管理機制。

因應 RxJava2 新的設計,RxBus 也提供了 toFlowable 的 Method,用來取得有背壓版本的 Observable。當然,要透過 toObservable 再轉換為 Flowable 也行,toFlowable 就只是做為提供一個便利、縮減程式碼的用途。

準備 Model

這裡的 Model 是二個 POJO,分別用來代表一筆 User 和 Todo 的資料內容。因為這部分並不是示範的重點,所以檔案的內容請自行參考 User.javaTodo.java

定義常數

常數主要的作用是以不同的數值來區分不同的資料種類,以及每一個資料種類因應需求所必須提供的功能。如同以下所展示的程式碼內容:

在需求中提到需要處理二種類型的資料,所以就分別定義了 DATA_USERDATA_TODO 來代表使用者及 Todo。以 User 的需求來看,在畫面上只會有載入資料的要求,以提供切換使用者的功能,所以 User 的動作只定義了 USER_LOAD。而 Todo 的需求就比較複雜,除了載入資料以外,還要可以新增、關閉 Todo。所以目前定義 TODO_LOADTODO_ADDTODO_CLOSE 等三個常數。

這些常數接下來會被用在 StoreMap 的鍵值及 Action 的 Type。在 FluxJava 中並沒有限定只能使用數值型別來做為鍵值,可以根據每個專案的特性來設定,可以是字串、型別或是同一個型別不同的 Instance。

撰寫 Action 及 Store

UserAction 和 TodoAction 都是很直接地繼承自 FluxAction。其中比較特別是:考量到一次可能會要處理多筆資料,所以在 Data 屬性的泛型上使用 List 來做為承載資料的基礎。這二個 Class 的內容請直接連上 Github 的 UserAction.javaTodoAction.java 二個檔案查詢。

Store 可以繼承 FluxJava 內建的 RxStore,在 RxStore 中  registerunregister 是提供給前端的畫面元件,做為向 Store 登記要接收到資料異動事件之用。與 RxBus 相同,RxStore 額外提供一個 toObservable,如果想要取得更多在使用 RxJava 上的彈,可以改為使用 toObservable

當外部所有的呼叫都是使用 toObservable 來進行訂閱,則不會使用到 IRxDataChange 的 Interface,這個介面是透過 register 訂閱時才會需要實作。

附帶一提,與 RxBus 相同,RxStore 也有提供 toFlowable 用來取得有背壓版本的 Observable,使用的方式與 RxBus 相同。

Tag 則是考量到同一個 Store 有可能要產生多個 Instance 來服務不同的畫面元件,所以仿照 Android 元件的方式,用 Tag 來識別不同的 Instance。像是在同一個畫面中,可能會因為需求的關係,要使用不同條件所產生的清單來呈現圖表。這時就有必要使用二個不同的 Instance 來提供資料,否則會造成畫面上資料的混亂。

至於 getItemfindItemgetCount 都是很基本在呈現資料內容時需要使用到的功能。其中 getItem 之所以限定一次只取得一筆資料,而不是以 List 的方式傳回,主要是為了符合 Flux 單向資料流的精神。如果 getItem 傳回的是 List,前端很有可能意外地異動了清單的內容,根據 Java 的特性,這樣的異動結果也會反應在 Store 所提供的資訊上。也就等於資料的清單在 Store 以外,也有機會被異動,這就違反了 Flux 在設計上所想要達成的資料流動過程。

當然,就算是只提供一項資料,前端也許改不了整個清單,但還是可以修改所收到的這單一項目,其結果一樣會反應回 Store 的內部。所以在示範的程式碼中,在 getItem 所傳回的是一個全新的 Instance。

在 RxStore 中有一個關鍵的 Method 是要覆寫的,那就是 onAction,是用來接收前端所推送出來的 Action。而 getActionType 可以用來指定特定的 Action 型別,避免收到不相關的 Action 通知。以 UserStore 為例,會有以下的內容:

可以看到之前定義的常數在這裡派上用場了,利用 Action 的 Type 可以區分出前端所接收到的指令。在這個 Demo 中,Store 的定位只是用來管理清單,清單的資料會由 ActionCreator 傳入,所以可以看到程式碼中只是做很簡單的載入工作,載入完即發出資料異動的事件。這個事件是定義在 Store 內部,每個 Store 都有定義自己的 Event,以便讓前端元件判別與過濾所想收到的 Event 種類。

在以上的 Method 程式碼中,使用了 RxStore 所提供的功能,在接收到 Action 的當下是以背景的 Thread 在執行,避免因為過長的資料處理時間導至前端畫面凍結。Method 的參數則是用以過濾 Action,讓指定的 Action 型別在 Bus 中被傳遞時才呼叫 Mehtod,減少程式碼判斷上的負擔。如果是同一個 Store 有多個 Instace 同時存在,在接收到的 Action 中可以加入 Tag 的資訊,以便讓 Store 判別目前傳入的 Action 是否為針對自己所發出來的。

而因為需求的關係,同樣的 Method 在 TodoStore 中就相對地複雜了一點:

主要是多了二種資料處理的要求:在新增時,前端會把新增的內容傳入,所以這裡很簡單地把收到的項目加入清單之中,就可以通知前端更新資料。至於在關閉 Todo 的部份,由於之前提到 Store 在 getItem 回傳的都是全新的 Instance,所以要先進行比對找出資料在清單中的位置,因為是示範的緣故,很單純地只寫了個迴圈來比對。找到了對應的位置後,直接以新的內容取代原本清單中的項目,再通知前端更新畫面。

如此,Action 與 Store 的撰寫工作就算完成了。同樣地,在這個階段的最後,執行寫好的測試程式來確認目前為止的工作成果。

撰寫 ActionHelper

FluxJava 已經內建了一個負責 ActionCreator 的 Class,這個 ActionCreator 使用 ActionHelper 來注入自訂的程式邏輯。可自訂的內容分為二個部份,第一個是決定如何建立 Action 的執行個體,第二個是協助處理資料格式的轉換。

以下是第一個部份的示範程式碼:

內容的重點就是依照先前定義好的常數來指定所屬的 Action 型別。

第二個部分就會有比較多的工作需要完成:

根據 Flux 文件的說明,ActionCreator 在建立 Action 的時候是呼叫外部 API 取得資料的切入點。所以 ActionHelper 提供了一個 wrapData 來讓使用 FluxJava 的程式有機會在此時取得外部的資料。在以上的程式中,還另外示範了另一種 wrapData 可能的用途。由於在前端會接收到的資訊有可能有多種變化,像是在示範中,要求載入 User 時只需要一個數值、在載入 Todo 時則要額外告知此時選擇的 User、在新增或修改 Todo 時則是要把修改的結果傳入。這時 wrapData 就可以適時地把這些不同型式的資訊轉成 Store 要的內容放在 Action 中,讓 Store 做後續的處理。

如果想要使用自訂的 ActionCreator,可以在初始化 FluxContext 時將自訂的 ActionCreator Instance 傳入,只是這個自訂的 ActionCreator 要繼承自內建的 ActionCreator,以覆寫原本的 Method 來達到自訂的效果。

組合元件

這次示範中,Flux 的架構橫跨整個 App 的生命週期。所以最合理的切入位置是自訂的 Application,這裡增加了一個名為 AppConfig 的 Class 做為初始化 Flux 架構的進入點,同時修改 AndroidManifest.xml 讓 AppConfig 可以在 App 啟動時被呼叫。

在 AppConfig 內增加一個 setupFlux 的 Method,內容如下:

重點工作是把之前步驟中準備好的 Bus、ActionHelper、StoreMap 傳入 FluxContext 的 Builder 之中,並且透過 Builder 建立 FluxContext 的 Instance。截至目前為止,後端準備的工作算是完成了,在目錄的結構上各位應該可以看出來,我把以上的 Class 都歸類在 Domain 的範疇之中。

撰寫 Adapter

Adapter 是用來供給 Spinner 及 RecyclerView 資料的 Class,同時在這次的示範中也是與 FluxJava 介接的關鍵角色,代表的是在 Flux 流程圖中的 View。在 MainActivity 中 Spinner 是用來顯示 User 清單,而 RecyclerView 是用來顯示 Todo 清單,所以各自對應的 Adapter 分別是 UserAdapter 及 TodoAdapter。

雖然這二個 Adapter 繼承自不同的 Base Class,但是都需要提供 Item 的 Layout 以便展示資料。所以先產生 item_user.xmlitem_todo.xml 二個檔。

準備好了 Item 的 Layout 就可以進行 Adapter 的撰寫工作,以下是 UserAdapter 的完整內容:

在 UserAdapter 的 Constructor 中,使用 FluxContext 來取得 Store 的 Instance。使用的第一個參數就是之前在常數定義好的 USER_DATA,第二參數的 Tag 因為本次示範沒有使用到所以傳入 Null。最後一個參數是把 Adapter 本身的 Instance 傳入,FluxContext 會把傳入的 Instance 註冊到 Store 中。當然,如果要在取回 Store 後再自行註冊也是可以的。

之後部份就是 Adapter 的基本應用,需要提供資料有關的資訊時,則是透過 Store 來取得。

在 Adapter 的 Constructor 中可以看到以 RxJava 的方式向 Store 進行訂閱的程序,可以用來接收資料異動的事件。傳入的 Action 型別參數是用來限定要收到的事件種類,被呼叫後的工作也很簡單,就是轉通知 Spinner 重刷畫面。由於是要更新畫面上的資訊,所以要回到 UI Thread 來執行,observeOn 被指定為 MainThread。如果同一個 Store 同時有多個 Instance 存在,和 Store 的 onAction 一樣,可以在 Event 中加入 Tag 的資訊,以減少無用的重刷頻繁地出現。

最後則是一個用來釋放 Reference 的接口,主要之目的是避免 Memory Leak 的問題,大部份都是在 Activity 卸載時呼叫。

以下是另外一個 Adapter - TodoAdapter 的內容:

除了因為是繼承自不同 Base Class 所產生的寫法上之差異外,並沒有太大的不同。重點是在接收事件的訂閱多了一個,用來當資料異動的情境是修改時,只更新有異動的 Item,以增加程序運作的效率。

接下來的工作就是把 Adapter 整合到 MainActivity 的程式碼中:

除了把 Adapter 傳入對應的畫面元件外,還有幾個重點。第一個是在 onStop 時要呼叫 Adapter 的 dispose 以避免之前提到的 Memory Leak 的問題。另外一個是在 onStart 時會以非同步的方式要求提供 User 的清單資料,在畫面持續在前景的同時,UserStore 完成資料載入就會觸發 UserAdapter、UserAdapter 再觸發 Spinner、Spinner 觸發 TodoStore 的載入、TodoStore 觸發 TodoAdapter、TodoAdapter 觸發 RecyclerView 等一連串資料更新的動作。所以可以在 Spinner 的 OnItemSelectedListener 中看到要求送出 TODO_LOAD 的 Action。

會選在 onStart  都做一次資料載入的要求是考量到 Activity 被推入背景後,有可能會出現資料的異動,所以強制進行一次畫面的刷新。

寫到這裡除了執行所有已完成的單元測試外,其實可以再回去執行一次 UAT,這時可以發現已經開始有測試結果轉為通過了。

撰寫 Integration Test

在繼續完成需求之前,先插入一個有關測試上的說明,使用 Flux 的其中一個重要原因就是希望提高程式碼的可測試性。所以在這次的示範之中,選擇以 Integration Test 來展示 FluxJava 可以達到的效果。

就像一開始提到的,用 Robolectric 來測試 MainActivity 被定位成 Integration Test。主要的測試目標是要確認整合起來後 UI 的行為符合設計的內容,此時當然不希望使用真實的資料來測試,簡單的說就是要把 Store 給隔離開來。

要達到這個目的可以由 FluxContext 的初始化做為切入點,以 Robolectric 來說,他提供了一個方便的功能,就是可以在測試執行時以 Annotation 中設定的 Applicaton Class 取代原本的 Class。 就如同以下程式碼所示範:

而在 StubAppConfig 中就可以對 FluxContext 注入測試用的 Class 來轉為提供測試用的資料:

這裡使用 StubAppConfig 做為切入點的示範並不是唯一的方法,在實際應用上還是應該選擇適合自己專案的方式。

如果在執行 UAT 希望也使用測試的資料來進行,以 FluxJava 來說當然也不會是問題,達成的方式在本次的示範中也可以看得到。原理同樣是和 Integration Test 相同,是使用取代原本 AppConfig 的方式。只是在 Espresso 裡設定就會麻煩一點,首先要增加一個自訂的 JUnitRunner,接著 build.gradledefaultConfig 改成以下的內容:

所以在執行 UAT 與正常啟動的情況下,可以在畫面中看到截然不同的資料內容,即代表 Store 代換的工作確實地達成目標。
Production Test

 

撰寫新增 Todo 功能

在這次的示範中,達成新增 Todo 的功能就只是很簡單地在 MainActivity 加上 Add Menu,透過使用者按下 Add 後,顯示一個 AlertDialog 取回使用者新增的內容完成新增的程序。以下是 menu_main.xml 的內容:

接著在 MainActivity.java 中加上以下的 Method:

用來讓使用者輸入資料的 AlertDialog 是用 DialogFragment 來達成,以下是 Layout 的內容:

程式碼則是如下所示:

再來就是讓 MainActivity 可以在使用者按下 Menu 時彈出 AlertDialog,所以新增如下的 Method:

執行所有的測試,看測試的結果沒有通過的不多了,距完成只剩一步之遙。

撰寫關閉 Todo 的功能

從最後一次 UAT 執行的結果可以發現,仍未滿足需求的項目只剩下關閉 Todo 最後一項。要達成這一項功能要回到 TodoAdapter,將 onBindViewHolder 改成以下的內容:

最後,執行最開始寫好的 UAT,非常好,所有的需求都通過測試,打完收工!





2017/7/4

如何在 Android Studio 中引用不在專案目錄中的 jar 檔

Android Studio 使用 Gradle 做為建置的工具在 Dependency 的管理上,節省掉很多煩人的工作,讓引用 Library 變成極為單純的作業,也讓開發人員可以更專注在重要的事項。透過 Gradle 的功能,要引用 Project 以外的 Library,只要在 build.gradle 中提供 Library 的識別名稱及對應版本編號,即可透過預先設定好的 Repository 來自動取得 jar,不需要再由人工下載、更新。

以下是一個簡易的引用示範:

就算是公司內部自行開發的 Library,也能在編譯、輸出 jar 後,上傳到私有的 Repository 中。再經由同樣的模式,在 build.gradle 中新增私有 Repository、要引用的 Library 資訊,就可以於 Coding 時使用 Library 中的功能。

例如:

縱使利用 Repository 的形式來管理 Dependency 可以滿足大部份的開發需求,但仍然有一些情境之下必須要使用獨立的 jar 來做為 Dependency。像是有一些小型的團隊,可能沒有足夠的資源來設置自己私有的 Repository,可是還是有跨專案共用功能的需求,這時就只能透過 jar 或共用 Android Studio 的 Module 來達成。

所幸在 Gradle 的設定中,除了使用 Repository 之外,還提供直接引用 jar 的設定方法。在 Android Studio 預設產生的 build.gradle 中都會有以下的內容:

這個內容代表了只要把 jar 的檔案放到 Project 結構中名為 libs 的目錄下,在 Coding 時就可以直接使用 jar 內提供的功能。

只是光這樣還是會有一些不足之處,若是以開發為業,勢必不可能只有一個 Project。團隊內部共用的 jar 在每開一個 Project 就要再複製一份到其下的 libs 目錄中,似乎不是一個好的 Dependency 管理模式,而把 jar 集中在所有專案之外的需求就應運而生。

這時就出現了一個疑問,原先的設定只有把 jar 放在 Project 內的 libs 之下,在 Project 之外的 jar 要如何設定 Dependency?

面對這一個問題,其實解決的方法也很簡單。之前 build.gradle 的例子中,dir: 'libs' 代表的是相對於 Project 的位置,也就是在 Project 之下的 libs 路徑。當 jar 在 Project 之外時,只要調整內容為 Project 之上的路徑即可。

以上面的例子來說,是假定所有的共用 jar 都被放在與 Project 平行的 shared 路徑之下,以下是目錄結構的示意:
+ Project A
  + app
    + libs
    + src
      + androidTest
      + main
      + test
+ Project B
  + app
    + libs
    + src
      + androidTest
      + main
      + test
+ shared
  - *.jar

透過以上的方式,就可以達成類似 Repository 的效果,當 shared 內的 jar 被更新後,Project A 與 Project B 都可以在 Source Code 中使用最新的功能。而當這二個 Project 所使用的 jar 有版本不一致的情況時,只要分別把對應版本的 jar 放到所屬 Project 的 libs 路徑下即可。










2017/6/12

Espresso 只做了半套的 Code Coverage

有在使用 Espresso 撰寫測試程式的人應該都知道,在 Android Studio 中 Android Test 類型的 Configuration 是不能使用 Code Coverage 的,最少在 Android Studio 2.2.2 仍然是如此。也就是說「Run 'xxx' with Coverage」的按鈕沒有辦法按,情況如下圖所示:


這點和 JUnit 的 Configuration 不同:



產出 Coverage Report 的第一步

以上的限制,對認真撰寫測試程式的人來說,會造成很大的不便。Code Coverage 是撰寫測試程式的一項重要指標,沒有了 Code Coverage 就如同在伸手不見五指的黑暗中行走,完全不知身在何處,有關更多 Code Coverage 用途的細節,請參考先前的這篇文章

不過,說是做了半套,代表並不是完全沒有辦法產生 Coverage Report。要在 Espresso 測試執行的過程中產生 Coverage Report,要先調整 build.gradle,在 buildTypes 設定中開啟 testCoverageEnabled 選項。

完成以上修改,並執行 ./gradlew createDebugCoverageReport 指令成功之後,就可以在 Module 的 /build/reports/coverage/debug/ 路徑下,以 Browser 開啟 index.html

以上的步驟在搜尋之後,都可以取到一堆的文件提供相關的操作細節。但本篇到這裡還沒有結束,這個方法僅適用於測試和待測對象在同一個 Module 中。如果是在不同的 Module 裡,由 Coverage 的 Report 頁面中,其實可以看出清單中的 Package 都只列出測試程式所在 Module 的 Class。


擴增 Coverage Report 的範圍

假設有一個如下所示的專案結構:
+ Sample
  + app
    + src
      + androidTest
      + main
        + com.sample.app
      + test
  + domain
    + src
      + main
        + com.sample.domain

其中 app 是 Android Application,domain 是 Android Library。如果把所有的測試 Class 放在 app 的 Module 之下,在 test 路徑下的測試可經由 Configuration 的 Code Coverage 畫面,來把 domain 中的 Class 列入 Report 產生的範圍。


androidTest 中的測試就沒有這麼直接了,以 Android 的 Gradle Plugin 官方文件內容來看,目前並沒有相關可調整的參數。所以這個階段的目標,就是讓 androidTest 產出的 Report 和 test 一樣,可以把指定位置的 Class 列入 Coverage 的範圍內。

要達到這個目標,首先要引入 JaCoCo 的 Plugin,透過 JaCoCo 的參數調整來突破原本的限制。在 JaCoCo 中有 sourceDirectoriesclassDirectories 二項參數,用來指定 Report 要涵蓋的範圍。以上面專案結構的例子來說,需要產生以下的內容在 app 的 build.gradle 中:

在這裡附帶說明一下,其實可以把以上的內容獨立成單獨的檔案,例如:jacoco.gradle,再透過 apply from: 'jacoco.gradle' 的方式引用。一來可以簡化 build.gradle 的內容、方便維護,二來可以避免調整 JaCoCo 的選項時,IDE 頻繁地出現要求同步的訊息。

另外有一點需要注意的是,createDebugCoverageReport 產出的 executionData 是以 *.ec 為名稱,與一般 JaCoCo 使用的 *.exec 不同。

設定好以上的內容之後,就可以執行 ./gradlew app:jacocoTestReport。和 createDebugCoverageReport 一樣會產出 html 格式的 Report,但不同的是 index.html 會在 app/build/reports/jacoco/jacocoTestReport/html 路徑下看到。這個路徑是預設的,可以透過參數調整。


修正 Coverage Report 的問題

到這裡所有的問題都解決了嗎?其實並沒有!照著以上的內容所產出的 Report 中,domain 下的 Class 不論測試怎麼做,在 Instruction 和 Branch 的 Coverage 都呈現 0%,所以顯然有一個環節出現了問題。

花了一番功夫發現 domain 在執行 jacocoTestReport 時都只會產生 release 的 Class。

就算是把 classDirectories 的路徑調整成 release,在產出的 Report 中 Coverage 依然是 0%。

目前解決方案是要調整 domain 的引用方式:

而在 domain 的 defaultConfig 下也要增加以下的設定:

使用以上內容再執行一次 ./gradlew app:jacocoTestReport,就可以由 Report 中看到,domain 下的 Class 不再全都是 0% 的狀態。


仍需改善的部份

雖然 Report 已經可以順利的產出,但是與 JUnit 可以在 IDE 中直接檢視 Coverage 情況的經驗相比,還是有差別。畢竟沒有辦法直接在 IDE 對照著 Code 就可以了解 Coverage 的情況,要在 IDE 與 Browser 間來回地切換其實非常地不方便。

原本有試著把 createDebugCoverageReport 產出的 executionData,經由以下【Analyze -> Show Coverage Data...】功能所顯示的 Window 載入。但這個功能似乎只能載入 *.exec 的檔案,就算是把 *.ec 改為 *.exec,載入之後 Coverage 的狀態也都呈現 0% 的結果。可能是真的有格式上相容的問題,所以才會把產出的 executionData 以 *.ec 來區格,避免被 Android Studio 載入。
只能期待未來 Android Studio 在改版時,能將這一部份的功能納進入,既然在 Plugin 都已經可以產出 Report 了,把 Plugin 的動作整合到 IDE 的 Coverage 按鈕上,應該不是什麼難事吧!
















2017/6/5

解決 Spock 與 PowerMock 的整合問題

在前一篇文章中,提到了如何在 Spock 中測試 Static 的 Method,以彌補 Spock 在這個部份的不足。當時使用的是 PowerMock 1.6.2,只不過隨著時間的推移,最新的 Mockito 與 PowerMock 組合,在與 Spock 的整合上並不順利。

Mockito 目前已經發展到第二版,但是要在這個版本的 Mockito 上使用 PowerMock,依據官方的說明仍然還停留在試驗性質的版本,目前最新可取得的版本是 1.7.0RC4。

如果使用前一篇文章提到的,以 @Rule 的方式來啟動 PowerMock:

原本可以順利執行的測試案例,在執行測試時會出現 NullPointerException

這個問題在網路上的資訊不多,如果無法解決,就要回到 JUnit + Mockito + PowerMock 的方案,原本 Spock 所帶來的優勢就失去了,是一個很兩難的抉擇。

所幸在研讀 PowerMock 的官方文件時,提到了一個新的功能。在 PowerMock 1.6.0 開始,PowerMockRunner 可以 Delegate 另一個 JUnit Runner,以取代無法使用 JUnit Rule 的情況。

而在 Specification 的 Source Code 中可以看到,Spock 是使用名為 Sputnik 的 JUnit Runner。

所以把之前的 Code 改成以下的內容:

build.gradle 可以更精簡:

透過 PowerMockRunner 的 Delegation,在 Spock 1.1 及 1.0 上,基本的功能都可以正常的運作,只不過會有以下額外的訊息出現。

Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST
Notifications are not supported when all test-instances are created first!

由於不是 Production 的 Code,所以只要能運作,有點訊息或是使用的是非正式的版本,都還在可接受的範圍內。







2017/5/30

善用 Android Studio 的異動管理功能

身為一個開發人員,每天的工作就是在不斷地異動 Source Code 中度過。增加新的、修改舊的、刪掉不要的,而每一個異動都會對應到特定的目的,像是為了新的需求、修改 Bug、重構程式等等。

很多時候,異動的目的在工作的過程中是混在一起的,例如開發新功能的同時,也有可能在修正之前的問題。在自己的工作環境中,這些異動混在一起通常都不會有什麼問題產生。只不過這些工作的成果終究是要交付出去的,而問題總在於這些目的卻不一定是在同一個時間點被交付。如果所有的異動都混在一起,要隔離出需要交付的部份,勢必要花費一番工夫才能辦得到。

而這樣的工作要靠人工來逐個 Block、逐個 File 來分辨識,不但耗時,同時也極有可能出現疏漏。因為一個修改就有可能牽涉到十幾個 Files,再加上 IDE 自動產生或管理的加一加可能就有成百上千之數。自己經手的異動都不一定能精確的掌握,更何況是數量在數倍、完全不是自己產生的內容。

人工應付不來,就得要靠工具的輔助。就如同在「如何寫好程式」一文中提到,善用工具是寫好程式的功課之一。以開發 Android 時所使用的 Android Studio 來說,雖然是由 IntelliJ IDEA Community 版本進化而來,但不代表功能上就很陽春。針對本文提到的問題,其實有內建了相當方便的功能,可以協助開發者解決這類工作上的問題。

Android Studio 提供的異動管理功能

Changelist

這是一個以 File 為單位,把異動內容給分門別類的功能。透過這個功能,可以把修改過的 File 進行分組。當有異動內容需要被交付時,可以直接以分好的組別為單位交付。像是要進行 Commit 時,則可以指定特定的 Changelist 來 Commit,不在分組內的 Files 則不會受影響。

要使用這個功能可以先進入 Version Control Tool Window,Menu 的位置在【View -> Tool Windows -> Version Control】。開啟之後可以看見如下圖示的內容:


在 Local Changes 的 Tab 中,可以看到有一個 Default 的字樣,這就是 Android Studio 預先產生好的 Changelist。如果沒有特別指定,所有被異動的 Files 都會被歸在這個 Changelist 之下。在操作上可以使用 Tool Window 中左方的按鈕來新增一個 Changelist,新增時可設定此 Changelist 為 Active,代表之後所有還沒被異動的 File,在異動後都會被歸到這個 Changelist 之下。

要在 Changelist 之間移動 File 也非常地直覺,可以使用拖拉項目的方式,或是在項目上按下滑鼠右鍵選擇【Move to Another Changelist...】即可。

當要進行 Commit 時,就可以在如下的「Commit Changes」畫面中,最上方的下拉清單選擇對應的 Changelist。

選擇不同的 Changelist 時,Changelist 的名稱會預設成為 Commit Message 的內容。

由於 Changelist 是以 File 為單位,所以會有一個限制是同一個 File 不能同時歸屬於二個 Changelist。一旦編輯了不在 Active Changelist 中的 File,Android Studio 就會出現以下的警告:


可以看見 Tab 上的檔名變成了紅色,這是 Android Studio 遇到異動衝突預設的反應,這部份可以透過點選畫面中最右方的按鈕來調整。

這時如果只是忘了切換 Active Changelist,可以選擇【Ignore】或是【Switch changelist】。但若真的是二個不同的修改項目都異動到同一個 File,那就得選擇一個適當的策略。

當修改的內容不會有交互的影響,也就是說二個修改項目的結果可以共存在同一個 File 之中,則可以選擇【Move changes】把 File 移到最先要被 Commit 的 Changelist 中。

反之,修改的內容是互斥的時候,就要先保留其中一個版本、還原回修改前的狀態後,再開始另一個項目的修改。這個方式在 Android Studio 中也有提供了對應的功能來達成,在這篇文章的稍後會提到。

Changelist 在使用的情境上,還可以用來區隔一定會修改,但卻沒有要 Commit 的 File。例如有一些程式運作時需要的設定檔,內容中記錄的是 Production 的參數,在開發時就必須要進行修改才能做偵錯。這時就可以預先新增好一個專用的 Changelist,把這類的 Files 在修改之後歸進去。未來在 Commit 時才不致一時疏忽,把開發環境的設定參數給 Commit,造成後續建置上的問題。


Label

Label 主要是作用在【VCS -> Local History -> Show History】的 Window 上,如下圖所示:
在 Window 的左側,可以看到第一個和第二個 History 項目中間,夾了一個 Sample Label 的文字,這個文字是使用【VCS -> Local History -> Put Label...】功能放上去的。

透過這個功能,可以在進行一些實驗性的調整之前,先標定好目前 Source Code 狀態。當調整不如預期時,就可以不用花精神去回想做了哪些的修改,再一一去做回復。有了 Label 就可以在 History 的清單中找到所標定的 Source Code 狀態,使用【Revert】的功能,直接回到調整前的狀態,相當地省事又有效率。


Shelf

字面上的意義就是架子,是一個用來擺放文件夾的架子。而文件夾則是前面所提到的 Changelist 的快照,所以當 Changelist 發生衝突時,就可以利用 Shelf 把 Changelist 當下的狀態保留起來,等到衝突的情況解決了之後,再把原本異動的內容還原回來。

要把 Changelist 放到架子上,可以從 Menu 中選擇【VCS -> Shelve Changes...】。
可以由上圖看到,畫面和 Commit 差不多。完成之後,會在 Version Control Tool Window 中多出一個 Shelf 的 Tab,同時被 Shelve 的 Files 會回到異動前的狀態。在 Shelf 的 Tab 上,可以管理 Shelve 過的項目,像是 Unshelve、Rename、Delete。

在 Unshelve 的過程中,如果沒有出現內容衝突,則會自動套用 Shelf 中保留的異動狀態。如果內容出現衝突時,則會顯示以下的 Window,要求決定所需套用的版本:
Shelf 除了應用在工作項目的切換之外,如果所開發的 Project 有多個 Branch,在 Branch 還沒有相互 Merge 之前,也可以使用 Shelf 來轉移、把異動過程套用在不同的 Branch 上。這一點在異動的 Files 數量龐大時,就可以顯現出效率上差別,一個批次就可以完成工作,不用再一個個 File 來比對,並且擔心是否有異動的內容遺漏了。


Patch

Patch 可以算是 Shelf 的外帶版本,外帶去哪?就是把異動的內容帶出 Android Studio 的環境之外。使用 Menu 中【VCS -> Create Patch...】的功能,可以把原本要新增到 Shelf 的項目,改為產生一個實體的 File。操作的畫面和 Shelve Changes 一模一樣,只是在按下 Window 上【Create Patch...】的按鈕後,會出現以下的畫面,以便指定 File 儲存的位置。
基本上 Shelf 的項目和 Patch 可以互換,在 Tool Window 中 Shelf 的項目上可以觸發 Create Patch 的動作,讓 Shelf 的項目轉成 Patch。反之,也可以在 Shelf 的 Tab 上 Import Patches 成為 Shelf 的項目。在產生 Shelf 項目和 Patch 時,還有一點最大的差異是 Patch 產生之後,並不會將內容回復到異動之前,而是維持修改後的狀態。

從 Menu 中選擇【VCS -> Apply Patch...】後,可以把 Patch 的內容套用回目前的工作環境中,套用的過程和 Shelf 差不多,遇到內容衝突時也同樣會出現相同的畫面,來決定要選用的版本。

在應用上,Shelf 的項目能做的 Patch 都能做,除此之外 Patch 還可以用來在不同的 Android Studio 環境之間移轉。可以用來將工作的狀態由公司的環境中移至家中的環境,以便在離開公司之後仍可接續未完成的部份。或者是可以把 Patch 交給不同的開發人員,用來進行協同合作、Review Code 等工作。


和版本控管工具的比較

如果在開發時使用 Git 做為版本控管的工具,其實以上的功能 Git 大多都可以做到。Android Studio 則是在原有的版本控管機制之外,提供不同的選項,對於不熟悉版本控管工具的人來說有莫大的幫助。而對於用慣了原本工具的人來說,要怎麼使用還是得看每個人的習慣、對工具的喜好程度。只不過在面對不同的情況之下,多學會一種工具的使用,在應對的策略上也能產生更多的彈性。