Android開發架構設計之健壯且可讀的安卓架構(下篇)

原文地址:http://blog.joanzapata.com/robust-and-readable-part-2-introducing-async-service/

              健壯且可讀的安卓架構(二)


我收到很多關於我的架構設計的評論和反饋,特別感謝每週安卓社區(Android weekly),你們中的一切人注意到我的架構中的一些弱點,或者一些替代方案,還有的人問我要源碼。也有許多人問我在結尾處所說的第二部分什麼時候寫。
在我寫完第一部分的幾周後,關於我如何開始開發CandyShopApp是一個模糊描述,我們在Redmill實驗室中,從CandyShop轉到Midpic。在這過程中,我得到了一個絕佳的機會去將這個架構去轉爲一個開源庫:AsyncService

一、基礎用法

這是一個基本的AsyncService的用法,通過用戶名稱去檢索用戶
@AsyncService
public class UserService {  
   public User getUser(String name) {
      return ...;
   }
}
當你在activity裏面注入這個UserService的時候,你可以在任何線程裏面回調getUser()這個方法,它會立即返回null並開始異步執行,結果會以一個消息的形式返回,你可以通過構造一個帶有合適的參數方法拿到這個結果。

public class MyActivity extends Activity {

   @InjectService public UserService userService;

   public void onCreate(Bundle savedInstanceState){
      // Runs injections and detect @OnMessage callback methods
      AsyncService.inject(this);

      // Starts method asynchronously (never catch the result here, would be null)
      userService.getUser("Joan");
   }

   // Callback method for getUser
   @OnMessage void onUser(User e) {
      // Runs on UI thread.
   }
}

注意:

AsyncService是在編譯的時候生成代碼,一個MyActivity類會在編譯的時候創建,當你調用inject(this)的時候,系統會通過反射來實例化這個注入器,這點非常重要,因爲在android上反射是非常慢的。
正如你所見,這個機制非常的像我在第一文章第一部分中所提到的eventBus,只是這次你不需要註冊和註銷事件總線,它會全部處理好。在Service裏面,你也不用通過事件總線去傳遞User這個對象,它僅僅是一個返回值,所以代碼會更加的簡潔。
那麼,現在回有人注意到使用事件總線會有一個巨大的缺點:
如果你有多個調用者在同一時間調用了getUser這個方法,onUser這個方法會在意想不到的時候回調很多次。而AsyncService可以解決這個問題,在AsyncService.inject注入的時候,它會綁定好回調者,這意味着你只會通過你自己注入的UserService實例來獲得你回調的消息。

當然,如果你需要接受來自整個應用程序任何地方的消息,這是一個非常有用的管理消息通知機制,你可以在回調函數上加上OnMessage(from=ALL)來實現

調用緩存:

當我在寫第一篇文章的時候,我所關心的就是不要讓用戶等待,所以我描述瞭如何直接展示一些東西給用戶,如圖所示:
快速提示:服務通過事件總線直接發送緩存數據,然後通過相關API調用併發送更新後的數據。通過使用一個特殊的線程(serial=CACHE)去處理新的請求,我確定緩存會直接發送,然後在通過一個特殊的線程(serial=NETWORK)去處理網絡請求,這樣我更容易處理get-after-post上的麻煩。
通過使用AndroidAnnomations@serial的註釋、 EventBus和snappyDB,代碼看起來是這樣的
@Background(serial = CACHE)
public void getUser() {  
   postIfPresent(KEY_USER, UserFetchedEvent.class);
   getUserAsync();
}

@Background(serial = NETWORK)
private void getUserAsync() {  
   cacheThenPost(KEY_USER, new UserFetchedEvent(candyshopApi.fetchUser()));
}
這樣,依舊需要很多個引用代碼去寫每個請求,AsyncService有這樣一個註釋:
@CacheThenCall
public User getUser(){  
   return ...;
}
這樣很簡潔,對嗎?它依舊以相同的方式在後臺工作。
如果getUser()方法帶參數,比如getUser(String name),你可以使用一個特殊的鍵值來標記這個緩存,但是這個鍵值必須在整個應用程序中唯一。
@CacheThenCall(key="UserService.getUser({name})")
public User getUser(String name){  
   return ...;
}

一般來說,緩存鍵值的默認值形式爲"<Class.name>.<MethodName>({arg1},{arg2},...)",所以在這個地方我們實際上並不需要去標記它。

錯誤管理

在上一篇文章中,我沒有寫到錯誤管理,在AsyncService中有這樣的錯誤管理機制:
@AsyncService(errorMapper = StatusCodeMapper.class)
@ErrorManagement({
   @Mapping(on = 0,  send = NoNetworkError.class),
   @Mapping(on = 500,  send = ServerInternalError.class),
   @Mapping(on = 503,  send = ServerMaintenanceError.class), 
   ...})
public class UserService {

   @ErrorManagement({
      @Mapping(on = 404, send = UserNotFoundError.class),
      ...})
   public User getUser(String username){
      return ...;
   }

}

正如你所看到的,AsyncService定義了一個ErrorManager,這是一個將Throwable錯誤類轉換成int型數據的接口,比如,通過exception提取HTTP請求的錯誤狀態碼,我們會馬上看見它。
如果一個錯誤在getUser()中發生了,那麼ErrorManager將會把這個錯誤轉換成一個int數據,然後去和@Mappingannotations裏的值匹配,如果匹配成功,那麼匹配成功的類會被實例化並當做消息發送。
一個基礎的ErrorMapper接口可以是這樣:
public class StatusCodeMapper implements ErrorMapper {

   @Override
   public int mapError(Throwable throwable) {
      if (throwable instanceof HttpStatusCodeException)
           return ((HttpStatusCodeException) throwable).getStatusCode().value();
      if (isConnectivityError(throwable))
         return 0;
      return SKIP;
   }
}
如果返回的是SKIP,那麼意味着這個錯誤沒有捕獲,那麼它會發送到UncaughtExceptionHandler(這個類是全局異常捕獲類,之前寫的文章裏有說明),如果沒聽過這個類,這個類就是所有你未捕獲的錯誤所去的地方。一些錯誤報告工具例如:ACRA\Crashlytic等,捕獲錯誤並報告。
也許第一次寫這些很反感,但你只需要這麼寫一次就好。之後就可以定義什麼錯誤發生在每個方法中。在Midpic這個項目中,我的ErrorManager有一點點大,因爲我們的服務端響應頭裏面有這些無意義代碼{code:"1002",message:"blah blah blah"},所以我通過匹配1002條錯誤代碼去讀取錯誤。這樣使得我的代碼完全鏡像服務器API。
關於ErrorManager的最後一點,如果在getUser(String name)上註釋了錯誤標籤,404映射到了UserNotFoundError,那麼在activity中你可以這樣捕獲這個錯誤:
@OnMessage 
void onError(UserNotFoundError e){  
    Toast.makeText(this, "User does not exist.", LENGTH_LONG).show();
}
當錯誤發生在getUser(String name)方法時,你可以更進一步的捕獲到用戶名。這樣,你可以定義一個參數在錯誤信息中:
public class UserNotFoundError {  
   public UserNotFound(@ThrowerParam("username") String username){
      this.username = username;
   }
   ...
那麼現在你可以更加準確的展示信息:
@OnMessage 
void onError(UserNotFoundError e){  
    Toast.makeText(this, String.format("User %s does not exist.", e.getUsername()), LENGTH_LONG).show();
}

結論

在這篇文章中,我介紹了一些AsyncService的優點,例如緩存和錯誤管理機制,如果想得到更完整的功能列表,請閱讀github項目。正如我們所看到的,這是一個對AndroidAnnotations,eventBus,snappyDB組合的提升,這個框架已經用在Midpic項目上,目前爲止還沒有發現任何bug。
如今AsyncService已經實現了它的價值在Midpic項目上,但是我希望它可以幫助到更多的人並且我已經將它發佈到社區上,我很樂意接受相關意見和反饋!

終於翻譯完了。。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章