承接上篇 從零開始的 Android 新專案(5):Repository 層(上),本文繼續介紹Realm、快取,以及統一的異常處理設計。
Realm
Realm在移動端資料庫中也算是比較有名的一款了,以其跨平臺和驚人的速度而聞名。啊,對了,還有文件多。
這裡要黑的就是文件問題,Realm雖然乍一看文件很多,但是老實說,寫的挺亂的。不過總體來說,實踐和應用中感覺還不錯,效能好,也比較方便,比起不穩定的DBFlow和麻煩至極的GreenDao來好了太多了,唯一的美中不足就是so比較大,會增大包的體積1MB。
引入
從Realm 0.90開始,用法與之前有了改變:
在root的build.gralde中:
1 2 3 4 5 6 7 8 |
buildscript { repositories { jcenter() } dependencies { classpath "io.realm:realm-gradle-plugin:0.90.1" } } |
然後在對應需要應用到Realm的,比如data module的build.gradle:
1 |
apply plugin: 'realm-android' |
即可使用Realm。
使用
使用起來也很方便,比如我們想要快取使用者的資訊
1 2 3 4 5 6 7 |
public class UserPo extends RealmObject { @PrimaryKey public String id; public String name; public String headerUrl; public long updateTime; } |
這樣就對應了一個表,其主鍵為id,另外有3列name, headerUrl, 以及updateTime。
如果想要查詢,只需要:
1 2 3 |
UserPo user = getRealm().where(UserPo.class) .equalTo("id", userId) .findFirst(); |
如果要寫入一條記錄:
1 2 3 4 5 6 7 8 9 |
User user = new UserPo(); user.setName(userInfoEntity.getNickName()); user.setId(userInfoEntity.getUserId()); user.setHeaderUrl(userInfoEntity.getHeaderImageUrl()); user.setUpdateTime(System.currentTimeMillis()); getRealm().beginTransaction(); getRealm().copyToRealmOrUpdate(user); getRealm().commitTransaction(); |
就是這麼簡單。
如果想要直接和Retrofit一起應用,去進行序列化,可以參考該Gist。
1 2 3 4 5 6 7 8 9 10 |
// 結合 Realm, Retrofit 和 RxJava (使用了Retrolambda以簡化符號)的例子。 // 讀取所有Person,然後與從GitHub獲取的最新狀態merge到一起 Realm realm = Realm.getDefaultInstance(); GitHubService api = retrofit.create(GitHubService.class); realm.where(Person.class).isNotNull("username").findAllAsync().asObservable() .filter(persons.isLoaded) .flatMap(persons -> Observable.from(persons)) .flatMap(person -> api.user(person.getGithubUserName()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(user -> showUser(user)); |
更多詳情可以去官網看,migration/relationship等等支援應有盡有,我只能說,文件實在太長太長了。
記憶體
記憶體,也就是直接使用變數儲存在對應repository中,如果非空則優先直接返回記憶體中的變數。
LruCache
LruCache限定了最大的entry數量,近期最少使用演算法保證了淘汰機制的合理性。使用場景如使用者資訊快取,會淘汰那些最近沒有訪問過的使用者的資訊快取。使用可參考Google官網:LruCache。
變數儲存
變數儲存很簡單,直接在Repository實現類中直接變數儲存上一次的返回結果,在下一次請求的時候優先使用記憶體快取。使用場景如請求後直接重新整理本地的變數,下次呼叫repository方法使用啊concat先返回記憶體裡的變數,然後再使用網路資料進行重新整理。
統一異常處理
作為Repository層,本身不會,也不應該去處理任何異常和錯誤(比如請求的錯誤碼),一切都將作為Exception異常拋給上層去做統一處理,而RxJava的onError機制也幫助我們能優雅地去做這件事。
Observable.error
類似在上一篇中提到的方法,我們可以使用Observable.error返回異常,供上層根據該異常做對應處理。無論是網路異常,資料庫異常,亦或是伺服器response異常等等,都可以進行分類建立對應的Exception類,拋給上層。
1 2 3 4 5 6 7 8 9 10 11 12 |
public static <T> Observable<T> extractData(Observable<MrResponse> observable, Class<T> clazz) { return observable.flatMap(response -> { if (response == null) { return Observable.error(new NetworkConnectionException()); } else if (response.getStatusCode() == ResponseException.STATUS_CODE_SUCCESS) { return Observable.just(mGson.fromJson(mGson.toJson(response.data), clazz)); } else { Logger.e(TAG, response.data); return Observable.error(new ResponseException(response)); } }); } |
Subscriber.onError
我們使用Subscriber的基類來處理通用錯誤,其他所有Subscriber繼承它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class MrSubscriber<T> extends DefaultSubscriber<T> { @Override public void onError(Throwable e) { super.onError(e); if (!handleCommonResponseError((Exception) e)) { if (e != null && e.getMessage() != null) { Logger.w(TAG, e.getMessage()); } showErrorMessage(new DefaultErrorBundle((Exception) e)); } } } protected void showErrorMessage(ErrorBundle errorBundle) { String errorMessage = ErrorMessageFactory.create(this, errorBundle.getException()); showErrorMessage(errorMessage); } protected void showErrorMessage(String errorMessage) { ToastUtils.show(this, errorMessage); } |
DefaultErrorBundle
是exception的wrapper,管理了其錯誤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class DefaultErrorBundle implements ErrorBundle { private static final String DEFAULT_ERROR_MSG = "Unknown error"; private final Exception exception; public DefaultErrorBundle(Exception exception) { this.exception = exception; } @Override public Exception getException() { return exception; } @Override public String getErrorMessage() { return (exception != null) ? this.exception.getMessage() : DEFAULT_ERROR_MSG; } } |
ErrorMessageFactory
是錯誤訊息工廠,根據exception建立對應的錯誤訊息提示,讓使用者不至於碰到錯誤莫名其妙。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/** * Factory used to create error messages from an Exception as a condition. */ public class ErrorMessageFactory { private static final String TAG = "ErrorMessageFactory"; private ErrorMessageFactory() { //empty } /** * Creates a String representing an error message. * * @param context Context needed to retrieve string resources. * @param exception An exception used as a condition to retrieve the correct error message. * @return {<a href="http://www.jobbole.com/members/57845349">@link</a> String} an error message. */ public static String create(Context context, Exception exception) { if (StringUtils.isNotEmpty(exception.getMessage())) { Logger.e(TAG, exception.getMessage()); } String message = context.getString(R.string.exception_message_generic); if (exception instanceof NetworkConnectionException) { message = context.getString(R.string.exception_message_no_connection); } else if (exception instanceof NotFoundException) { message = context.getString(R.string.exception_message_not_found); } else if (exception instanceof ResponseException) { message = exception.getMessage(); } else if (exception instanceof HttpException) { message = exception.getMessage(); } return message; } } |
handleCommonResponseError
通常,伺服器會返回錯誤資訊,我們需要根據一些code進行對應處理,MrSubscriber的onError就呼叫了handleCommonResponseError
來處理這些通用錯誤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
protected boolean handleCommonResponseError(Exception exception) { boolean handled = false; if (exception instanceof ResponseException) { ResponseException responseException = (ResponseException) exception; switch (responseException.getStatusCode()) { case ResponseException.ERROR_CODE_NEED_LOGIN: handled = true; getUserSystem().setVuser(""); getNavigator().navigateToLoginPage(this); break; case ResponseException.ERROR_CODE_NEED_PERFECT_PROFILE: handled = true; if (responseException.getVuser() != null) { getUserSystem().setVuser(responseException.getVuser().getVuser()); } getNavigator().navigateToPerfectProfile(this); break; case ResponseException.ERROR_CODE_NEED_THIRD_PARTY_BIND: handled = true; getNavigator().navigateToThirdPartyBind(this); break; } } return handled; } |
Log & 上報
出錯了當然要上報啦,bugly、友盟,本地寫檔案打zip包上傳,Logger做的就是寫檔案log了,這些常見的app都會去做,這裡就不贅述了。
總結和下集預告
本系列兩篇文章描述了Android專案中,Repository層的設計與實現,也可以理解它為data或者model層。一個好的Repository層和上層相對獨立,內聚完成業務邏輯的資料部分,即便內部有修改,比如新增了快取,對外仍然保持一致。而好的異常處理設計一方面讓程式碼中不會充斥著雜七雜八的 try & catch,另一方,恰當的錯誤展示也讓使用者知道究竟出了什麼錯,不至於莫名其妙。
下一次不知是何時相見,希望能為大家帶來我們專案中使用React Native進行混合開發的苦與甜。
另外,打個小廣告,本司的新產品Crew已經在各大Android應用市場上線,專注於職場垂直社交。一搜和興趣相投的人聊天。iOS版本正在稽核中。
2個字找到志趣相投的職場夥伴,秒搜陌生人同類,智慧自動破冰。多關鍵字疊加,高效率鎖定職場同僚。精準匹配興趣物件,超輕聊天,更能一鍵組建群聊,加入一群人的狂歡。
demo沒空寫了,反正我也沒混淆,直接反編譯來黑我吧。哈哈。有bug或者功能上的意見建議歡迎直接反饋給我。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式