2016/12/31

FluxJava 與 Rx 結合的使用示範

這篇是「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 的管理機制。

準備 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 訂閱時才會需要實作。

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 改成以下的內容:

同時調整 Android Studio 的 Configuration 中指定的 Instrumentation Runner 內容如下:

所以在執行 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,非常好,所有的需求都通過測試,打完收工!





2016/12/7

FluxJava 與 EventBus 結合的使用示範


這篇是「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 的文章。接著就是把程式碼填入,完成後的內容如下所示:

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

撰寫 Bus

依照 Flux 架構,需要為整個資料循環建立 Dispatcher。但是在 FluxJava 中 Dispatcher 的功能是以 Bus 的方式實作,所以實際上是要先準備 Bus 的 Class。在這次的示範中使用 greenrobotEventBus 來簡化開發工作,並且包裝在實作 IFluxBus 的 Interface 內,以便整合進 FluxJava 的 Framework 內。程式碼的內容如下:

與 Bus 搭配的測試用 Class 的內容如下:

執行以上 Class 之後,確認測試通過並檢視 Code Coverage。如果測得到的程式碼都有被涵蓋,就可以確認目前完成的程式有一定的穩定度,可以繼續往下進行接下來的工作。接下來的幾個小節都會採用這樣的工作節奏來逐步推進,以其望在程式完成時能夠有一定基礎的品質。

準備 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 內建的 FluxStore,或是自行實作 IFluxStore 的 Interface。在 IFluxStore 中  registerunregister 是提供給前端的畫面元件,做為向 Store 登記要接收到資料異動事件之用。

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

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

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

在 Store 中有一個關鍵的 Method 是 FluxStore 中沒有、要自行增加的,那就是用來接收前端所推送出來的 Action。由於目前使用的是 EventBus,以 UserStore 為例,會有以下的內容:

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

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

使用 EventBus 的 Annotation 規格宣告 Method 時,在 Android Studio 上會有一個即時語法檢查的警告出現,相關的處理細節可以參考這一篇文章

而因為需求的關係,同樣的 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 的尾端可以看到有一個和 Store 類似的 Method,因為同樣是使用 EventBus 來傳送資訊,所以使用相同的方式來接收資料異動的事件。同樣地,在 Method 的參數上以型別來限定要收到的事件種類,被呼叫後的工作也很簡單,就是轉通知 Spinner 重刷畫面。由於是要更新畫面上的資訊,所以要回到 UI Thread 來執行,threadMode 被指定為 MainThread。如果同一個 Store 同時有多個 Instance 存在,和 Store 的 onAction 一樣,可以在 Event 中加入 Tag 的資訊,以減少無用的重刷頻繁地出現。

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

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

除了因為是繼承自不同 Base Class 所產生的寫法上之差異外,並沒有太大的不同。重點是在接收事件的 Method 多了一個,用來當資料異動的情境是修改時,只更新有異動的 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 改成以下的內容:

同時調整 Android Studio 的 Configuration 中指定的 Instrumentation Runner 內容如下:

所以在執行 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,非常好,所有的需求都通過測試,打完收工!





2016/12/2

如何消除間接呼叫 Method 在 Android Studio 中的編譯警告

如果有使用過 DI (Dependency Injection) Framework 像是 butterknife,或是 Bus 一類的 Framework 像是 Otto 或 EventBus,一定會撰寫出 Method 是沒有被自己開發的程式碼直接呼叫的。這時在 Android Studio 的即時編譯檢查就會出現警告:

Method ‘...’ is never used

就像這篇文章裡提到,警告的出現會形成一定程度上的困擾,最好是能夠消除掉,畢竟這是一個無法避免的寫法,但又不應該是個問題。

照「調整 Android Studio 編譯檢查規則」中的方法把檢查規則整個取消掉,是個一勞永逸的方法。但是也把其可能的錯誤也抑制掉了,徒增程式品質上的風險,並非上策。

另外一個可行的選項是在方法上宣告 SuppressWarnings 的 Annotation,像是 @SuppressWarnings("unused")@SuppressWarnings({"UnusedDeclaration"})@SuppressWarnings({"UnusedParameters”})。跟前一個方案一樣,應該顯示的警告也同時被遮蔽掉了,不是最佳的方式。

所幸 Android Studio 也不是沒有提供對策,在有問題的程式碼上按下「alt+enter」,會跳出 Menu 並可以選擇 Suppress for methods annotated by ‘...’ 項目。以 greenrobotEventBus 為例,所有以 org.greenrobot.eventbus.Subscribe 為 Annotation 的 Method 都不會再顯示警告,所以問題順利地解決了。

以上的解決方法應該已經很多人都知道了,只是還有一個衍生的問題。如果使用的 Framework 提供的 Annotation 不只一個,還是得要一個一個地重覆相同的動作,不能一次搞定嗎?

這時可以開啟「調整 Android Studio 編譯檢查規則」中提到的 Inspections 選項畫面,切換到「Java->Declaration redundancy->Unused declaration」項目。畫面右方的 Options Frame 最下方有一個【Configure annotations...】的按鈕,點下去之後會再跳出另一個小視窗。接著就可以在視窗的最下方新增【Add Annotation Class】或【Add Annotations Pattern】,前一個是新增單一個 Class,另一個則是使用萬用字元來增加一整組的 Class。


以上說明的這些資訊都被存在 .idea/misc.xml 的檔案中,打開後會看到類似以下的內容:

如果你有莫名的控制慾,想要凡事都掌握在手中,可以直接修改檔案的內容,也會達到一樣的效果。








2016/11/28

設定 build.gradle 來用 Spock 對 Android 元件進行測試

在「使用 Android Studio 開發 Web 程式 - 測試」中說明了為什麼要選擇 Spock Framework 來做為測試時的 Framework。在實作時 build.gradle 要做一定程度上的設定,才能夠使以 Spock 寫出來的程式碼得以運作,以下將會說明相關設定上的細節。

首先,Spock 的測試程式碼需要使用 Groovy 語言來撰寫,所以 build.gradle 中要先引用 Groovy 的 Gradle Plugin。第一步要在 Root 的 build.gradle 中增加以下內容:
第二步是在要使用 Groovy 的專案 build.gradle 中增加 apply plugin: 'groovyx.android’ 的內容,讓 Gradle 可以正確地識別 Groovy 所寫的程式。

第三步要設定在封裝 Apk 的過程中被排除的檔案:
完成以上的工作就可以設定 Dependencies 以便在撰寫程式時可以引用到 Spock Framework 裡的 Class。但是光只有 Spock 也只能對一般的 Class 進行測試,如果要測試 Android 元件,牽涉到 Context 的問題,還是要使用官方的 Espresso 或是 Robolectric

Espresso 的問題不大,只要把 Java 的寫法改成 Groovy 套到 Spock 的 Class 結構裡就行了。Robolectric 則是要改為引用另一個套件:Robospock,依照 RoboSpock 的官方文件的說明,就可以順利的使用 Spock 來測試 Android 的元件。RoboSpock 就已經有引用 Robolectric 所以自己的 build.gradle 並不需要再設定一次。只是必須受限於 RoboSpock 建置時所設定的 Robolectric 版本,不一定能使用最新版的 Robolectric。

最後一個要注意的事項是,Espresso 在執行前是要經過實際封裝 Apk 的過程,所以 Groovy 在引用時要使用特別為 Android 開發的版本 - org.codehaus.groovy:groovy:2.4.7:grooid,否則很容易會出現超出 64K 的問題。

以下是完整的 build.gradle 的示範內容:
撰寫 Spock 測試前要先手動在 androidTest 或是 test 目錄下增加 groovy 的目錄,其下再依據所屬的 Package 產生路徑結構。

最後,由於相容性的關係,在編譯的過程中會持續出現以下的訊息,但實際操作時並沒有阻擋或影響到測試程式的執行,所以可以忽略、不用理會。
:module:transformClassesWithDexForDebugAndroidTest
AGPBI: {"kind":"error","text":"warning: Ignoring InnerClasses attribute for an anonymous inner class","sources":[{}]}
AGPBI: {"kind":"error","text":"(groovyjarjarantlr.TokenStreamRewriteEngine$1) that doesn\u0027t come with an","sources":[{}]}
AGPBI: {"kind":"error","text":"associated EnclosingMethod attribute. This class was probably produced by a","sources":[{}]}
AGPBI: {"kind":"error","text":"compiler that did not target the modern .class file format. The recommended","sources":[{}]}
AGPBI: {"kind":"error","text":"solution is to recompile the class from source, using an up-to-date compiler","sources":[{}]}
AGPBI: {"kind":"error","text":"and without specifying any \"-target\" type options. The consequence of ignoring","sources":[{}]}
AGPBI: {"kind":"error","text":"this warning is that reflective operations on this class will incorrectly","sources":[{}]}
AGPBI: {"kind":"error","text":"indicate that it is *not* an inner class.","sources":[{}]}
AGPBI: {"kind":"error","text":"warning: Ignoring InnerClasses attribute for an anonymous inner class","sources":[{}]}
AGPBI: {"kind":"error","text":"(groovyjarjarantlr.build.ANTLR$1) that doesn\u0027t come with an","sources":[{}]}
AGPBI: {"kind":"error","text":"associated EnclosingMethod attribute. This class was probably produced by a","sources":[{}]}
AGPBI: {"kind":"error","text":"compiler that did not target the modern .class file format. The recommended","sources":[{}]}
AGPBI: {"kind":"error","text":"solution is to recompile the class from source, using an up-to-date compiler","sources":[{}]}
AGPBI: {"kind":"error","text":"and without specifying any \"-target\" type options. The consequence of ignoring","sources":[{}]}
AGPBI: {"kind":"error","text":"this warning is that reflective operations on this class will incorrectly","sources":[{}]}
AGPBI: {"kind":"error","text":"indicate that it is *not* an inner class.","sources":[{}]}
AGPBI: {"kind":"error","text":"warning: Ignoring InnerClasses attribute for an anonymous inner class","sources":[{}]}
AGPBI: {"kind":"error","text":"(groovyjarjarantlr.debug.misc.ASTFrame$1) that doesn\u0027t come with an","sources":[{}]}
AGPBI: {"kind":"error","text":"associated EnclosingMethod attribute. This class was probably produced by a","sources":[{}]}
AGPBI: {"kind":"error","text":"compiler that did not target the modern .class file format. The recommended","sources":[{}]}
AGPBI: {"kind":"error","text":"solution is to recompile the class from source, using an up-to-date compiler","sources":[{}]}
AGPBI: {"kind":"error","text":"and without specifying any \"-target\" type options. The consequence of ignoring","sources":[{}]}
AGPBI: {"kind":"error","text":"this warning is that reflective operations on this class will incorrectly","sources":[{}]}
AGPBI: {"kind":"error","text":"indicate that it is *not* an inner class.","sources":[{}]}






2016/11/16

A Tutorial for FluxJava

This document will show you how to use FluxJava step by step. You can find all the sources in Github.

For someone may not familiar with Flux, here is a good place to start.

Start from BDD

In this document, I will demo the usage of FluxJava by developing a simple todo app. The process will start from a BDD flow.

First, we need to define our requirements. Below is the list of requirement for the simple todo app:
  • View todo list.
  • Switch list between different names.
  • Add a todo.
  • Close/Open a todo.
Then we need a story to describe these requirements, it should look like this:
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

Ok, since it is just a demo, the content of story has been simplified and it should be more complicate in the real cases.

Plan how to test

There will be one test class base on Espresso treated as UAT to verify the story, since I only wrote one story. For the non-Android component classes, they will be followed by a test class as unit test. The rest classes that base on Android component will be tested with Robolectric and treated as integration test.

In order to make the covert process from story to the test cases easier, I will use Spock Framework to replace JUnit. And Java will be replaced by Groovy, when writing the test cases.

Here is the dependencies for test:
  • Groovy
  • Spock Framework
  • RoboSpock
  • Espresso
RoboSpock is an Android testing framework which brings Spock Framework and Robolectric work together. It comes with Robolectric, so you don’t need to add the dependency of Robolectric again. But you also limited to use the version of Robolectric that RoboSpock works with.

Here is the sample for how to configure build.gradle with these frameworks:

Create the UI

After created a empty Android project, the first thing is modify MainActivity. In this Activity, we will need a RecyclerView and a Spinner to show the data of todos and users. The list of todo can be switch from Spinner. Here is the layout:

Create UAT

To contain the test files, we need to create a folder named groovy under androidTest and delete java one. If you make the build.gradle right, you will see the groovy folder with background color shows it is a test folder. Now, we can put our test file 'ManageTodoStory.groovy' into groovy folder with package structure.

Here, we can see the advantage brought by Spock, the codes almost the same with the origin story.
Then we can put the real test codes into the class as below:
There are some steps of resources creation have been skipped.

Create the bus

According to Flux pattern, dispatcher is the central hub to deliver messages. In FluxJava, it uses bus to replace the functionality of dispatcher. So, we have to create a bus instead of a  dispatcher.

There is a IFluxBus interface required by FluxJava when you create a bus. In this demo, I used EventBus from greenrobot to make the code simple. Here is the codes:
If you use Rx add-on of FluxJava, you won’t need to create a bus. There is a built-in bus class named RxBus.

Create the models

The models in this demo are two POJOs. They are used to contain single user and todo data. you can check the content of these two class from 'User.java' and 'Todo.java'.

Define the constants

This not the necessary step but it will make codes more clear. As you can see, there are two constants that point to user and todo data type. Each data type has it’s own actions base on the description from story.
DATA_USER and DATA_TODO will be used as key of StoreMap latter. In FluxJava, you can use any type of data as a key of StoreMap not just integer. It could be a string, a type of class, a instance of class.

Create the actions and stores

UserAction and TodoAction inherit from FluxAction directly. The type of their data property is List, so they can contain more then one item when they pushed by dispatcher. you can check the content of these two class from 'UserAction.java' and 'TodoAction.java'.

To create a store, you can inherit from FluxStore in FluxJava (for Rx add-on it will be RxStore) or implement IFluxStore. register and unregister of IFluxStore are provided for UI components to tell store that they need to get the event of data change.

If you have multi-instance of a store in you application, tag will be useful to distinct which instance of store you are relate to.

The methods getItem, findItem, getCount are basic operations for inquiry data to display. The reason of getItem not return a list is to avoid any list modification outside store. It still has possibility that the content of single item changed by non-store class. So, it recommends create a new instance before return from getItem method.
Now, we need to let store get actions from dispatcher. For EventBus case, you can add a method as below:
For RxStore, you need to overwrite onAction method:
The action constants defined in previous step are used here. In UserStore, for example, there is only one action - load user data. All the user data will prepared by ActionCreator in this demo and the data will be sent with action. What store need to do in USER_LOAD is put the data into list.

Then store can emit an event to tell listeners that data is ready. The events store need to emit are define inside the class in this demo.

Both EventBus and Rx case, it will need to put job into background thread to avoid block UI. In EventBus, I added an annotation attribute to specify working thread is background. In Rx, the job will be done by RxStore.

You don’t need to worry about the action type when action pass in. They will be filtered by the parameter type of method or getActionType method from RxStore. But you need to worry when there are more then one instance of store waiting to receive actions. In this case, you need to put tag information into actions before you send.

Below are the results of TodoStore for EventBus and Rx:
Two more cases were added into codes compare to UserStore. TODO_ADD is simply put the new item get from action into list and inform the listeners that list has been updated. TODO_CLOSE need to find the position of list due to getItem return a different instance. For demo purpose, I wrote a very simple loop to do the job. Once the item has been located, it will be replaced by new one and the listeners will be informed.

Create ActionHelper

ActionHelper is used to customize the process of action creation and the process will be done by ActionCreator in FluxJava.

First thing that ActionHelper can do is decide which type of action to create instance. Second part is translate the data format.

In above codes, the ActionHelper choose the type of action by the constants defined by previous step.

There are more jobs to be done in the second part of ActionHelper. In Flux documents said, you can make a remote call to get data in Actions Creator before create an action. To follow this, you can write the remote API calls in wrapData of ActionHelper.

Another usage of wrapData shows in above codes. It could be different format when get data from UI components. For example, it will be an integer when asking load user data, a string when asking load todo data, a todo item when modify todo. It is a good place in wrapData to change these forms into uniform one.

If you don’t like what ActionCreator in FluxJava did, you can inherit it and overwrite the methods. Then put your own ActionCreator instance into FluxContext when initializing it.

Put the components together

It’s time to put all things together. The Flux flow is cover all the application lifecycle in the demo, it is good choice put initialize jobs in Application class.

I wrote a AppConfig inherits from Application and modified the 'AndroidManifest.xml' to make it work. In AppConfig, there is a method call setupFlux as below:
How you put all components together is using builder inside FluxContext and pass the instance of classes that were created in previous steps.

Create the adapters

The adapters are used to feed data to Spinner and RecyclerView in MainActivity. They also play a role to connect the FluxJava framework. In this demo, they will be the views of Flux pattern.

I wrote two adapters UserAdapter and TodoAdapter. The former provides user data for Spinner to display. And the other provides todo data for RecyclerView. Before create these adapters, we need to create item layout first. Check the content in  'item_user.xml' and 'item_todo.xml'.

Here is complete UserAdapter:
The UserAdapter get the store instance in constructor. The first parameter of getStore is USER_DATA that was defined in 'Constants.java' and also a key of StoreMap. So FluxContext can get the store that we want.

The second one is tag for getting store instance, but we don’t use here, so pass null. The last one need pass a instance of view that want to get events from store. You also can do it by your self after get store instance using it’s register method.

In the rest part of UserAdapter, store keeps the data list for adapter, so UserAdapter just call store when it need.

There is a similar method with the stores in the end of UserAdapter. It utilize EventBus to get events too. In the same manner, the parameter type will limit the events passed in. By specify the annotation attribute, the execution back to the main thread in order to access UI components and only one thing need to do in this demo - notify Spinner that data has been changed.

You also need to put tag into events, if you have more than one store will emit events.

The last one method is used to release references that will avoid memory leak issue.

For Rx add-on, you won’t need to add a method to receive events, instead you can call  subscribe from the Observable that exposed by toObservable of RxStore.
The content of another adapter, TodoAdapter, show as below:
There are not much need to explain, just one method was added and it is used to get another event. It can make RecyclerView to distinct what to refresh, the whole list or just one item.

Want to work with Rx add-on? Like UserAdapter, just use subscribe instead of add methods. See the sources here.

Ok, we are ready to connect adapters with UI components. Here is the MainActivity:
As you see, nothing special when setup RecyclerView and Spinner. There will be a chain reaction from onStart. First, a request will be sent out to load user data. After user data loaded, Spinner will trigger a request to load todo data. Then you can see the data shows in the Spinner and RecyclerView.

Why did I put the request in onStart  I want to make sure the Activity get the most update data when it back to foreground, no matter it was changed or not.

The dispose method of Adapters are called in onStop of Activity to release resource before it close, then we will get free from memory leak issue.

Create integration test

To make apps more testable is one of the reasons I introduce Flux in my codes. When running a integration test, we don’t want to use real data. Isolate stores is an option when we make integration test.

In this demo, it shows how to archive this goal in FluxJava. Robolectric provides a convenient way to inject stub Application when a test start. So I wrote a StubAppConfig and specify in annotation attribute:
In StubAppConfig, I replaced the production classes with test ones. After did this, the data retrieved by UI components are from the test data source.
If you want to do the same thing in UAT or Espresso, you can replace the Application class too. But the different is you have to create a custom JUnitRunner as below:
The last step is specify the new JUnitRunner in Configurations of Android Studio:

When you run the UAT, you will see the different with the production one:
Production   Test
 

 

Create add todo feature

Let’s continue to enhance the app. It is a simple process for how to add a todo, just let user tap the add item in menu and show a AlertDialog to get data, then done. Obviously we need create a menu layout first:
Then put the menu into MainActivity:
I used DialogFragment to show the AlertDialog, so here is the layout:
And the codes:
Add codes in MainActivity to show the AlertDialog when user tap the menu:
After running the test, we know we finished the add todo feature, cool!

Create close todo feature

It’s the last feature we are not complete. To make it happen just back to TodoAdapter, modify onBindViewHolder as below:
Run the test, you will find all the cases passed. All done, have fun!



2016/11/12

FluxJava: 給 Java 使用的 Flux 函式庫

為何選擇 Flux


設計上遇到的問題

最初在接觸 Flux 時就有一種驚豔的感覺,長久以來在設計上所出現的困擾似乎出現了曙光。在 Flux 還沒有出現之前,MVx 系列 (MVC、MVP、MVVM) 的 Design Pattern 就一直引領風潮。這類型的 Design Pattern 成功地解決了特定的問題,但卻也形成了某些尾大不掉的隱憂。在畫面不多、顯示資訊單純的應用程式中問題不容易顯現,但隨著程式複雜度的昇高,設計上所隱含的矛盾也不住地增強。

MVx 系列的設計在概念上是一個畫面對應一種資料類型,畫面專責顯示與處理該類型的資料。很直覺、也很有效地把功能區分成一組、一組的單元。水能載舟亦能覆舟,正所謂成也蕭何、敗也蕭何。就是因為每一組 MVx 太過獨立、區隔性太強,當出現整合式畫面的需求時,會造成在設計上進退兩難的抉擇。

假設程式中有一個畫面叫 Dashboard,需要整合客戶、訂單、存貨的資料。試問,這時是要設計一個新的 Model 納入所有資料?還是打破規則讓一個 View 對應多個 Model?

有人也許會問:這是問題嗎?

如果只是期望程式能夠執行,那的確算不上是個問題。但是如果要考慮到程式碼的可維護性,就必須要維持在設計上的一致性,這點在程式愈複雜的情況下愈顯重要。否則就不需要搞什麼 Design Pattern,就隨性而為、讓一切都歸於渾沌就好了。

再舉另一個例子,假設要開發的是線上購物的訂單畫面,下單時要提供客戶資料、訂單資料、刷卡資料。依據之前的原則,所有的資訊都會被設計納在一個單一的 Model 內。當某一天高層突然下指令要把購物流程改成 Wizard 的方式,每個步驟各自獨立成一個畫面。試問在這樣的情況下,開發新畫面時是讓 Model 拆解成多個?還是維持原本的樣子?

如果要維持原本的樣子,由於每一組的 MVx 都是獨立的,如何傳遞 Model?誰要負責控制傳遞的順序?又該如何保留 Model 的狀態?好吧!那就拆開...

拆開之後,問題似乎解決了,但此時高層又說了,這個程式要跨平台,所以二種類型的畫面都要有...

Flux 所提供的效果

Flux 的架構則是打破這層膠著的狀態,在其單向資料流的原則之下,View 只要管顯示資料,不管資料的來源是一個還是多個。而被通知資料有異動時,也是依循相同的方式來獲取資料,刷新畫面。至於要如何異動資料與 View 無關,只要把異動的資訊傳出去,接著就像戰機上的飛彈一樣可以射後不理。

在這樣的設計之下,以之前 Dashboard 的例子,不管是單一的畫面負責顯示所有的資料,還是畫面上分割成許多不同的元件來分別顯示特定的資料,都不會有設計上的違和感。而另一個例子同樣也適用,無關後端的資料規劃方式,View 只要專注在選墿合適的資料來源、考量如何顯示資料上即可。

Flux 只能用在有 UI 的情境之下?不儘然,並不是只有人才會輸入或需要取得回應。在有明確的邊界之狀況下,像是網路或是因設計的考量所形成邏輯上的 Layer,這種可以用來把資料供給端及接收端做有效的分離,以便進行分工、測試等等作業的架構,都可以考慮套用 Flux 的概念。

如何實作

俗話說得好,知易行難。了解 Flux 的運作過程是一回事,但要把這些過程落實到設計之中、形成程式碼又是另外一回事。Facebook 並沒有為 Java 的開發環境開發一套符合 Flux 的函式庫,而 Java 的環境相較於 JavaScript 又更加地多元化,加大了使用上的不確定性。為了避免在開發上每次都要反覆進行類似的工作,於是就依據過去的工作經驗,利用抽象化的手法及自動生成的概念,實作了一個 Framework,讓想要在 Java 的專案中使用 Flux 的人可以輕易的上手。

接下來會針對這個 Framework 做個簡單的說明。

取得 Binary

最新版本的 Jar 檔可以在 Github 的 Release 頁面中下載。

設定

如果是使用 Gradle 來建構程式,則所下載到的檔案可以送到 build.gradle 設定參照的目錄下。如果是 Android 的專案,則是放到 libs 的目錄下即可。

在專案中有使用 fluxjava-rx 時,應該也會需要在 build.gradle 中增加以下的內容:

在使用之前

在 Github 的 Repository 中,FluxJava 的函式庫程式碼放在 fluxjava 的目錄下,並且在 demo-eventbus 目錄下搭配一個示範用的 Android 專案。這個示範的專案是一個只有單一 Activity 的簡易 Todo 應用程式。在這個 App 中可以展示以下的功能:
  • 顯示 Todo 清單
  • 在不同使用者間切換 Todo 清單
  • 新增 Todo
  • 關閉/重啟 Todo

在這個示範專案中使用 greenrobot 的 EventBus 來協助 Dispatcher 和 Store 發送訊息。

如果想要與 RxJava 搭配使用,可以看一下 fluxjava-rx 目錄,裡面有 FluxJava 為 RxJava 所開發的 Addon。同時,有一個與之配對的示範專案在 demo-rx 目錄下,是由 demo-eventbus 複製過來修改的。在這個示範專案中,原本的 EventBus 以 fluxjava-rx 所提供的 RxBus 取代。而基於 RxJava 1.x 函式庫的 RxBus 所提供的功能和 EventBus 的功能相同。

如何使用


準備工作

  • Bus
    Dispatcher 和 Store 會呼叫 Bus 來傳送訊息。Bus 必須要實作 IFluxBus 的介面,實作時可以使用任何的 Bus 方案,像是:Otto、EventBus,或是自行開發的方案。如果有同時引用 fluxjava-rx,則可以直接使用 RxBus 來提供傳送訊息的功能。
  • Action
    Dispatcher 使用 Action 來通知 Store 要進行的工作。在 Action 中有二個屬性,一個是 Type、一個是 Data。Type 用來讓 Store 識別要對資料進行的動作,Data 則是該動作的附屬資訊。以示範的專案來說,當一個新的 Todo 從介面上被傳進來,則新 Todo 的內容會被放在 Data 欄位中。
  • ActionHelper
    ActionHelper 協助 ActionCreator 決定產生何種 Action,並且協助 ActionCreator 將目前傳進來的資料格式轉成可被處理的格式。
  • Store
    Store 負責截收由 Dispatcher 所送出的 Action,並根據 Action 上的資訊進行對應的資料處理。當資料處理完成,Store 會再送出一個資料異動的事件,讓事件的接收者可用以反應新的資料狀態。
  • StoreMap
    StoreMap 是一個一對一的對照表,在 Framework 中使用這一個對照表來產生需要的 Store Instance。假設 Action 和 Store 的關係是一對一的,則 Action 的型別可以用來做為 Store 型別的鍵值。像是在示範的專案中可以看到有二個 Action,分別是 UserAction 及 TodoAction,並且各自會對應到一個 Store 的型別。因此,與 TodoAction 配對的 TodoStore 就會被產生來負責處理與 Todo 相關的資料要求。

初始化程序

在 FluxJava 中,FluxContext 是用來做為整個程序開始的進入點。FluxContext 被設計成 Singleton,負責整合 Framework 中相關的元件,並且管理特定元件的 Instance。

FluxContext 的 Instance 可以由其內含的 Builder 來建立,示範的程式碼如下:

開始發送要求

在取得使用者透過 UI 元件所輸入的資料後,接下來可以利用 ActionCreator 來推送 Action,ActionCreator 的 Instance 可經由 FluxContext 來取得。Framework 預設所提供的 ActionCreator 只有一項功能 sendRequest,呼叫的程式碼要傳入 Id 及使用者輸入的資料。其中,Id 是用來決定要產生的 Action 型別。使用者輸入的資料可以在呼叫 sendRequest 後,經由 ActionHelper 轉成 Store 所需的格式。

以下為示範的程式碼:
sendRequest 有提供二種版本的實作,同步和非同步。非同步的版本會先建立一個新的 Thread 之後,在新的 Thread 中執行。如果需要特別管控 Thread 的使用或是想要使用 Thread Pool,則可以呼叫同步的版本來達到目的。

進行資料處理

要進行資料處理需在 Store 中攔截指定的 Action,攔截的方法會依據所使用的 Bus 方案而不同。以示範專案的例子來說,要在 Store 中新增一個搭配特定 Annotation 的方法。相關的程式範例如下:
如果是使用 fluxjava-rx,則 Store 可以繼承自 RxStore,此時只要覆寫 RxStore 中的 onAction 方法即可。相關的程式範例如下:

反應資料異動

跟 Store 一樣,UI 元件要依據使用的 Bus 方案來接收由 Store 所發出的資料異動事件。在 EventBus 的例子中:
在 RxBus 的例子中:










2016/10/28

如何在 Drawable 的 Selector 中使用 Theme 所定義的 Color 值

在最新的 Android 版本中提供了一項方便的功能,可以指定 Theme 中定義好的顏色到 Layout 中的對應 Attribute。例如:在 Theme 中會定義 android:colorPrimary 來代表主要的顏色,此時可以指定 TextView 的 textColor 為 "?android:attr/colorPrimary”,則 Text 的顏色就會呈現 colorPrimary 所設定的數值。當 Theme 的 colorPrimary 更改時,TextView 上的 Text 顏色也會隨之修正。

不過這並不是這個功能的優勢,在 textColor 中指定 “@color/primary”,再透過修改預先在 res/values/colors.xml 中定義的顏色也可以達到同樣的效果。這項功能最主要的好處是 textColor 會隨著所在的 Theme 而呈現不同的顏色值。如果有一個 Layout 含有上述的 TextView,且在不同的 Activity 間共用,但每一個 Activity 都有指定自己專屬的 Theme。此時,例子中 TextView 上的文字顏色在不同的 Activity 上就會隨著 Theme 而改變,甚至是啟動 Activity 前另外指定 Theme 也會讓 textColor 隨之不同。

這也就是為什麼在最新的 SDK 中呼叫特定的函式時,都會被建議要加上 Theme 的參數。因為在 Layout 中有可能指定 Theme 中某一個內容,如果沒有一併指定 Theme 時,會造成這些資訊沒有辦法取得而出現問題。

如果想要讓 View 在不同的 State 中顯示不一樣的顏色可以使用 ColorStateList 的 Selector 來達成效果。但對 android:background 的 Attribute 不適用,在 android:background 中要指定 Drawable 類型的資源。最直接的方法就是同樣的 Selector 檔案移到 res/drawable 路徑下,原本的內容中 android:color 要改成 android:drawable。

只不過此時會有一個惱人的問題出現,在使用 android:drawable 時沒有辦法以 ?android:attr 的方式來指定 Theme 中所定義的顏色。這下就有點傷腦筋了,豈不是要回復到之前每一個 Theme 都要建立一組 Selector 的麻煩方式。同時,在某些情況下要用程式指定 Selector 時還要先判斷目前的 Theme 來決定產生的 Instance,平白多打很多的程式碼。

所幸,經過上網查詢之後找到了解決的方法,可以將 Selector 改用以下的方式替代:





2016/9/22

軟體的盈利模式

當辛辛苦苦開發好一個應用程式、一套系統,如果想要期望這個軟體能夠養家活口、甚至是榮華富貴,就必須要思考一個問題:如何向使用者收錢?

軟體可以收錢的模式,大致可分為幾種類型:
  • 依功能計價
  • 依資料容量計價
  • 依使用人數計價
  • 依使用時間計價
  • 依內容與服務計價

2016/8/4

如何使用程式更改 Activity 中 App Bar 的背景顏色

一般在設計 Activity 時,會設定 Activity 的佈景主題,也就是套用某一個特定的 Theme。如果今天在啟動 Activity 後想要動態調整 App Bar 的背景顏色,可以在另外指定 Activity 所套用的 Theme 後重新啟動 Activity。然而,這樣的程序只是為了換個顏色似乎有一點小題大作,所以在這篇文章中提供了另外一種不用重新啟動 Activity 的方法。

2016/7/29

在可收合的 App Bar 中加入 Subtitle

可收合的 App Bar (以前叫 Action Bar 後來又一度改成 Tool Bar) 是 Android 平台上新推出的 Material Design 效果。要在開發的 App 中使用這個效果並不難,只要在最新的 Android Studio 中,於新增 Activity 時選擇【File -> New -> Activity -> Scrolling Activity】,並依照「Configure Activity」視窗的欄位填好內容,按下【Finish】按鈕,就可以有一個執行起來如下圖的畫面,頗為無腦。

2016/7/17

解析 Android 開發時的 SDK 版本參數設定

在開發 Android App 時,開發人員最容易出現困惑的地方之一應該是 compileSdkVersion、minSdkVersion 和 targetSdkVersion 這些參數的用途。對此,有 Google 內部的 Android 開發人員就這個議題寫了一篇文章:Picking your compileSdkVersion, minSdkVersion, and targetSdkVersion。在這裡會用不同的角度來說明這些參數的用途,希望能對大家在應用上有所幫助。

2016/6/25

行動裝置的資料存取選項

今天如果是要開發一個在行動平台上的應用程式,應用程式的功能要夠豐富,就勢必得牽涉到資料存取的議題。否則使用者在操作程式時,就如同和海底總動員的 Dory 對話一般,每次開啟程式都只能反覆地進行基本的作業、無法累積操作狀態的資訊。

2016/5/30

使用 PMP 的 EVM 追蹤軟體開發進度

軟體開發在追蹤上的難題

一般在企業內部要開始一個軟體開發的專案有很多時候是老闆忽然靈機一動地問說:「我們來開發一個某某系統,你覺得需要多久?」有經驗的人聽到這個問題時就知道這是考驗專業能力與政治敏感度的時刻,因為這是一個兩難的問題。在好傻好天真的時期會想:「天啊!系統要做到什麼程度、有多少人參與、要給多少資源都還沒確定,就要押完成日?」會有這樣的想法不意外,就像是要蓋一個房子,連要蓋幾層都不確定、設計圖都還沒畫,更別提建材和工人都還沒有著落的情況下就要問何時入住,回答得出來才有鬼!

2016/3/25

驚!點鈔機進化到足以全面取代人工作業

日前 DeepHand 發表了一款新的點鈔機取名為 BetaCo,該點鈔機具備了可自我調整力道的滾輪、高精度的感應器,並且提昇了對於各種鈔票狀況的學習能力,能夠在收集一定數量的資料後適應並清點各種不同情況的紙鈔。雖然前一代的點幣機成功地擊敗了世界知名的點幣好手,但由於紙鈔具有柔軟的特性,而且有可能因為髒污、破損、折痕等各種情況導致機器在判讀上的失誤,難度相較於硬幣要高出好幾個等級,所以這一款點鈔機問世對於產業界來說有一定的指標意義。

DeepHand 的技術人員透過大量地餵入各種情況的鈔票,來讓機器收集資料、建立樣本庫,以便機器在面對不同情況的紙鈔時能更準確的調校內部機構的出力及感應器的門檻值,正確地算出鈔票的張數。為了更進一步驗證機器的能力,於是 DeepHand 向世界排行有數的點鈔快手發出比賽的邀請。

比賽由目前排名第四的選手出陣,一共進行五局,進行的方式是每局準備一落十九疊張數不等的舊鈔,共十九落,由雙方先後以計時的方式進行點鈔並各自記下結果。下一局對調雙方的點鈔順序,由前一局後數的一方先進行點鈔。結果於該局雙方點鈔完成後同時公開,如果雙方點出的張數都正確,則以進行的時間決勝負、時間較少者獲勝。

結果,戲劇性地 BetaCo 以壓倒的秒數就取得前三局的勝利,贏得與人類對戰的比賽。接下來的二局雖不影響賽果,BetaCo 仍以一勝一負優異的成績作終,在這次的比賽 BetaCo 以四比一的比數大勝對手。唯一落敗的一局,是因為對手先攻的情況之下,在數鈔時目視到二張完全貼合的鈔票但未將其分開,而後攻的點鈔機判斷失誤以致少算一張而出現數字不正確,將勝利拱手讓人,否則以時間來看機器還是技高一籌。

對於這樣的比賽結果,大多數的人認為是人類的最大失敗、該點鈔機將全面取代所有的人工。於是人人自危、擔心飯碗不保、生計無以為繼,更甚者有人喊出人類末日到了。針對這樣的後續發展,人們是否應該開始省思點鈔機所可能帶來的危害、是否該針對點鈔機的發展計劃做出限制?



以上內容皆為杜撰、如有雷同純屬模仿。應該不會有記者真的把上面的內容拿去做成一篇報導,最後引起軒然大波吧...

不過,這篇內容和軟體開發有什麼關係?BetaCo 應該是沒有,但 AlphaGo 就義意重大了!有寫過程式的人應該都會同意,程式是開發團隊對於特定領域經驗的一種延伸。軟體開發過程中的一項核心工作就是把要被程式取代的工作內容給規則化,以便開發人員可以編寫成程式碼。

開發團隊的成員就像是在戰場上擬定戰略的主帥,主帥憑著自己的經驗預想戰場上可能出現的情況,並且逐一設定好應對的標準程序。一旦開戰主帥只能置身事外,完全讓部隊照著程序執行,如果一切和預想的一樣,則戰勝的結果可期。然而,事先訂好的規則畢竟是死的,再怎麼周詳仍然會有盲點。所以,反之,出現料想之外的情況,前線部隊將無所適從,則會一路潰敗,以致兵敗如山倒。由此可知,如何把規則明確且周詳地定義出來,這是團隊的功力所在,也是程式品質優劣的關鍵。

以圍棋這個領或來說,寫能下棋的程式不是什麼問題,市面上隨便一本入門書都已經把所有的規則定義好了,只要按圖索驥即可。只是這樣的程式並沒有辦法下贏對手,甚至有可能輸了都不知道。倒底要如何才能贏?我想就連當今棋王也不見得能夠說出個所以然!AlphaGo 的開發團隊能夠把抽象的圍棋致勝規則給歸納出來,並且落實到程式中,還能夠真的在棋局中獲勝。對於現階段的軟體開發工作而言,是一項了不起的成就、一個重要的里程碑。也讓在軟體界打滾了一陣子,但才能平庸、學藝不精的我來說是一個難以望其項背的層次。

至於 AlphaGo 是否有可能統治人類?也許吧!但就現階段來說,應該還是跟一般程式一樣有其侷限性,大概就是止於模仿人類學習圍棋的程度,否則這次就不會只是挑戰圍棋了。所以要說 AlphaGo 有什麼更進一步的能力似乎還早了點,因為歷來這麼多棋手也沒看過有哪個職業被棋手取代,即使 AlphaGo 贏了棋,最應該擔心失業的是棋手才對吧!就像對點鈔機稍有概念的人看到這個文章的標題,應該都會一笑置之,畢竟點鈔機是專門設計用來點鈔,要用來清點書本頁數都不太可能!



2016/2/25

使用 iWork 的 Pages 來撰寫軟體開發文件

撰寫軟體開發文件在團隊裡往往都是一個讓人愛恨交織的課題,開發人員的身體裡大多都帶著討厭寫文件的基因。所以在時程不充裕的案子中,文件撰寫一定是背負著拖累時程的原罪、頭一個被捨棄的工作項目。但是在接手既有系統時,又期昐原本系統的開發相關人員能留下文件,哪怕是文字殘缺不全、沒有隨著最新的程式碼同步修正,就算只有隻字片語也多少能做為一窺系統設計全貌的契機。

文件本來的目的就是用來溝通(不過制度僵化的公司也許會拿文件來做神主牌),而用什麼工具來撰寫文件會是個影響這項工作成效的關鍵因素。用 iWork 的 Pages 來撰寫軟體開發文件?用慣微軟產品組合的人第一個想法可能會是:為什麼這麼想不開?既然是要溝通,撰寫出來的文件內容就要具備可交換性。如果只是要文件內容可以被閱讀,那麼能輸出成類似 PDF 的通用格式的工具問題都不大。但如果牽涉到協同合作、共用編輯之類的情境,文件的格式轉換工作將會是一場災難。就算是微軟的 Word 在不同作業系統之下,呈現出來的效果也沒有辦法百分之百完全相同,更何況是使用二個設計概念不同的工具要進行轉換。

再者,以功能豐富度的角度來看,使用 Microsoft Word 上手之後,會覺得 Pages 像是 Windows 裡的 WordPad 加強版。Microsoft Word 在發展了這麼多年之後,考慮到許多內容呈現、版面排列、資訊彙整等除了文字以外附帶的功能,甚至也有為亞洲語系的特殊需求做設計,都大幅地增加文件製作者在使用上的彈性。反觀 Pages 在功能上則感覺不到有任何重大地進展或是想要向 Word 致敬的意圖,就是維持在大概夠用的程度。

的確,考慮到文件的可交換性,Pages 並不是個值得推薦的選項,除非整個團隊的工作環境都籠罩在蘋果光之下,不是使用 OSX 就是以 iOS 做為平台。如同大多數以 Windows 為平台的團隊會選擇使用 Microsoft Word 做為文件撰寫工具的第一首選,因為以 Word 普及的程度,就算開發團隊以外傳入的文件也有很大的機率是使用 Word 來撰寫。而 Pages 功能的豐富度上也遠不及 Microsoft Word,如果可以接受使用 Windows 裡的 WordPad 來撰寫開發文件、或是更原始地使用記事本來製作文件,在 OSX 上 Pages 才相對地是可以列入考慮的選項。

那為什麼要還要考慮使用 Pages 來撰寫文件?

隨著 iOS 成為行動裝置平台的顯學,改變了軟體開發的生態與銷售模式,為軟體開發從業人員帶來了不同的機會。想要靠開發 iOS App 來賺錢,受限於 Apple 的策略,所有開發的工作環境就必須要和 OSX 綁在一起。但日常事務性的作業卻不見得可以一併移轉,仍然要在 Windows 的平台中進行,而形成雙作業系統的狀態。如果要像過往在 Windows 平台一樣,以 Word 為撰寫文件的工具,就算公司有購置 Word 授權,也是 Windows 平台上的授權沒有辦法移轉到 OSX 上。所以勢必需要二份的授權費用,對許多團隊來說要再提撥額外的授權預算是沈重的選項。

所以,在 Apple 宣佈 iWork 免費下載之後,在不考慮其他開源的文書編輯軟體的情況下,Pages 是目前在 OSX 上可以直接取得、堪用、門檻最低的選擇。

只不過要拿 Pages 來撰寫文件在作法上還是要有一些調適,文件的可交換性降低了,就沒有特別期待能夠使用於共同編輯的情況,主要是著眼在做為「記錄設計軟體過程中思考路徑」的用途之上。設計通當都是反覆構思的循環,在設計的初始階段,為了釐清設計的重點,Mind Map 是一個值得推薦的工具。隨著工作的演進,我習慣用文字記錄設計上的一些想法,做為進一步文件的基礎。一般的純文字編輯器並沒有辦法滿足我的需求,畢竟思考是一個很容易被中斷的活動,像是生理時鐘、外在環境的干擾,甚至思考本身也會因為總總的想法造成設計上的分歧點,需要不斷的放下、回復。使用 Pages 可以做一些顏色、排版上的變化來幫助我記錄思考中斷的位置,讓我可以接續原本思考的狀態,做為後續調整設計方向的依據。

Pages 只是功能上和 Word 相比有一些不足之處,但仍是一個功能健全的文書編輯工具。如果是講究一點,還是可以套用 Pages 內建提供的範本,讓這些內容能夠直接輸出成一份俱專業感的正式文件。至於要和團隊中既有不同工具的文件體系銜接,就只能依靠 Pages 的格式轉換功能,和後續版面調整的功夫了。




2016/1/29

來自 Play Store 的流量垃圾 (Referrer Spam)

長見識了,原來流量垃圾 (Referrer Spam) 的「轉介網址」跟廣告信的「寄件信箱」一樣,可以被設定成任何的網址。不過想想也不意外,HTTP 裡本來就有轉介規格,如果 Blogger 是利用這個規格來記錄流量來源,那送出要求端要設定成什麼字串就隨人高興了。

最近在檢視 Blogger 提供的「統計資料」功能時,發現有一個集中的流量來源是轉自 Play Store (play.google.com),看網址是指向某個 App。至於哪一個就不附上來,被騙已經心有不甘了,不想再替他廣告,讓更多人點進去。

看到這個網址當下的想法是,會不會有人在評論中加上了文章的網址。但想想又有點奇怪,有什麼樣的 App 評論需要附上別人的文章?佐證?讓我變相去站台?

基於好奇就點了那個網址,想看一下是什麼人?寫了什麼樣的評論?結果,是一個交友的 App,新上架的,根本還沒有任何人留下評論!那是有客服或連絡資訊誤植了文章的網址?我知道,很白痴的想法!但我還是檢查了一下,的確沒有。

讓我意識到這應該是一個流量垃圾的原因是:簡介的內容全部都是俄文,是個戰鬥民族開發的軟體。一個連英文簡介都沒有的 App,怎麼可能會和中文為主的 Blog 有關係?!雖然我不清楚這個廣告是怎麼鎖定我的文章,也沒有直接的證據顯示這是個流量垃圾,但我高度的懷疑,就算是網址中的 Domain 和常見的流量垃圾 Domain 不同。

所以還是留下個記錄,讓收到相同資訊的人有個戒心。我覺得應該還不致於有病毒或木馬,畢竟是在 Play Store 上的標準頁面,主要的目的應該還是增加曝光度吧!

在查詢相關資料時,發現了一個有趣的小插曲。Referrer 因為經常被誤拼為 Referer,反而積非成是地被列入規格文件之中!