第一期的文章比較匆忙囤屹,遺留了好多問題。最明顯的一個(gè)是ViewModel如何獲取詳細(xì)的個(gè)人信息逢渔。假設(shè)用戶信息是從網(wǎng)絡(luò)獲取肋坚,那么我們調(diào)用后臺(tái)接口即可獲取數(shù)據(jù)。如果后臺(tái)是REST API的,使用Retrofit作為網(wǎng)絡(luò)請(qǐng)求就再合適不過了智厌。
至于什么是REST API诲泌,簡(jiǎn)言之就是把后臺(tái)所有請(qǐng)求看成是一種對(duì)資源的操作。比如關(guān)注一個(gè)人就是@Post把被關(guān)注的人的信息放到我的關(guān)注列表里铣鹏,取關(guān)一個(gè)人就是@Delete從我的關(guān)注列表里刪除這個(gè)人的資源敷扫。而這些接口的api都是相同,參數(shù)也是一樣的诚卸,不同的訪問方式不同葵第,產(chǎn)生不同的效果。更好的的解釋可以點(diǎn)擊鏈接合溺。
獲取數(shù)據(jù)
那好卒密,讓我們用Retrofit的WebService獲取數(shù)據(jù)吧。
public interface Webservice {
/**
* @GET declares an HTTP GET request
* @Path("user") annotation on the userId parameter marks it as a
* replacement for the {user} placeholder in the @GET path
*/
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}
是不是立刻想在ViewModel里面獲取數(shù)據(jù)辫愉,然后根據(jù)LiveData展示數(shù)據(jù)呢栅受?這樣做是沒有錯(cuò)的,但是隨著項(xiàng)目的增長(zhǎng)恭朗,這會(huì)讓項(xiàng)目變得難以維護(hù)屏镊。比如現(xiàn)在的問題來了(這個(gè)例子不一定合理),如果在查詢網(wǎng)絡(luò)之前我想先查詢下本地?cái)?shù)據(jù)庫中有沒有這個(gè)用戶的信息痰腮,那我就要在ViewModel中這段代碼之前添加查詢數(shù)據(jù)庫的操作而芥,這樣ViewModel的代碼就會(huì)變得非常臃腫。更重要的是這違反了上面提到的Soc原則膀值,ViewModel承擔(dān)了太多的功能棍丐。此外ViewModel的作用域是和Activity/Fragment綁定的,如果Activity/Fragment正常退出沧踏,相應(yīng)的ViewModel就會(huì)釋放掉相關(guān)的數(shù)據(jù)歌逢,當(dāng)用戶再次進(jìn)入這個(gè)頁面就會(huì)再次進(jìn)行網(wǎng)絡(luò)請(qǐng)求,這顯然在某種程度上是浪費(fèi)的翘狱。所以View的生命周期一結(jié)束就失去了所有數(shù)據(jù)秘案,這在用戶體驗(yàn)上會(huì)造成困擾。那怎么辦呢潦匈?
把這部分功能通過一個(gè)新的Repository類代理執(zhí)行阱高。這個(gè)Repository類的作用就是獲取并提供各種來源的數(shù)據(jù)(數(shù)據(jù)庫中的數(shù)據(jù),網(wǎng)絡(luò)數(shù)據(jù)茬缩,緩存數(shù)據(jù)等)赤惊,并且在來源數(shù)據(jù)更新時(shí)通知數(shù)據(jù)的獲取方。
可以這樣來寫UserRepository
:
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// This is not an optimal implementation, we'll fix it below(這不是最好的實(shí)現(xiàn)凰锡,以后會(huì)修復(fù)的)
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// error case is left out for brevity(為了簡(jiǎn)單起見未舟,簡(jiǎn)化了錯(cuò)誤處理的情況)
data.setValue(response.body());
}
});
return data;
}
}
目前來看就是把網(wǎng)絡(luò)請(qǐng)求的代碼從ViewModel中移到了UserRepository
中而已圈暗,似乎顯得非常多余。但是考慮到數(shù)據(jù)來源的多重性处面,這樣做可以簡(jiǎn)化ViewModel中的代碼厂置。更重要的是他抽象出了數(shù)據(jù)來源這一層,使更換數(shù)據(jù)來源變得非常簡(jiǎn)單(參考上面那個(gè)不一定合理的請(qǐng)求用戶信息的例子)魂角,對(duì)于ViewModel來說只是獲取到了數(shù)據(jù),并不知道數(shù)據(jù)的是從網(wǎng)絡(luò)獲取的還是從本地獲取的智绸。
上面的代碼中為了簡(jiǎn)單起見忽略了錯(cuò)誤處理的情況野揪,這在實(shí)際應(yīng)用中是不合理的。詳細(xì)的解決方案在后面的文章中有提供瞧栗,先讓我們跳過這一部分斯稳。
管理組件間的依賴關(guān)系
從上面的代碼不難看出UserRepository
里需要WebService
這個(gè)類來完成網(wǎng)絡(luò)請(qǐng)求。所以就要在UserRepository
里創(chuàng)建WebService
這個(gè)類迹恐,這就會(huì)帶來很多麻煩挣惰,假如WebService
的構(gòu)造函數(shù)變了,那么用到WebService
的地方都需要更改殴边,這會(huì)讓代碼變得難以維護(hù)憎茂。
有兩個(gè)解決這個(gè)問題的辦法:
Dependency Injection 依賴注入 Google推薦的依賴注入方案是Dagger2〈赴叮可以看下我寫的另一系列關(guān)于Dagger2的文章:
從實(shí)例出發(fā)理解Dagger2(一)
從實(shí)例出發(fā)理解Dagger2(二)
從實(shí)例出發(fā)理解Dagger2(三)
從實(shí)例出發(fā)理解Dagger2(四)
從實(shí)例出發(fā)理解Dagger2(五)
從實(shí)例出發(fā)理解Dagger2(六)
從實(shí)例出發(fā)理解Dagger2(七)Service Locator Service Locator設(shè)計(jì)模式提供了一種如何獲取依賴而不是構(gòu)造依賴的方法竖幔。這比依賴注入理解起來要簡(jiǎn)單的多,如果你對(duì)依賴注入不熟悉是偷,可以采取這種方法替代拳氢。
連接ViewModel和Repository
接下來使用Dagger2和UserRepository
來提供數(shù)據(jù)來源。代碼如下:
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
@Inject // UserRepository parameter is provided by Dagger 2
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// ViewModel is created per Fragment so
// we know the userId won't change
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser() {
return this.user;
}
}
緩存數(shù)據(jù)
UserRepository
很好的解決了數(shù)據(jù)層抽象的問題蛋铆,但是每次請(qǐng)求數(shù)據(jù)都只有網(wǎng)絡(luò)請(qǐng)求一種方法馋评,會(huì)顯得功能上有點(diǎn)單一。而且ViewModel每請(qǐng)求一次用戶信息都會(huì)重新請(qǐng)求網(wǎng)絡(luò)一次刺啦,即使是已經(jīng)請(qǐng)求過的用戶信息留特。這會(huì)造成兩個(gè)問題:
- 浪費(fèi)流量
- 讓用戶有不必要的等待
為了解決這個(gè)問題,可以在UserRepository
里添加內(nèi)存級(jí)的緩存UserCache來解決這個(gè)問題洪燥。
代碼如下:
@Singleton // informs Dagger that this class should be constructed once
public class UserRepository {
private Webservice webservice;
// simple in memory cache, details omitted for brevity(簡(jiǎn)單的內(nèi)存緩存)
private UserCache userCache;
public LiveData<User> getUser(String userId) {
LiveData<User> cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData<User> data = new MutableLiveData<>();
userCache.put(userId, data);
// this is still suboptimal but better than before.
// a complete implementation must also handle the error cases.
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
data.setValue(response.body());
}
});
return data;
}
}
持久化數(shù)據(jù)
上面的做法已經(jīng)可以解決用戶重復(fù)打開相同的頁面數(shù)據(jù)獲取的問題磕秤,但是如果用戶退出了App,那么這些數(shù)據(jù)也就被銷毀了捧韵。隔了一段時(shí)間用戶重新打開相同的頁面仍然需要從網(wǎng)絡(luò)獲取相同的數(shù)據(jù)市咆。解決這個(gè)問題的最好辦法就是提供本地持久化的數(shù)據(jù)Model。說了這么多再来,終于到了Android Architecture Components的另一重要內(nèi)容:Room蒙兰。
Room is an object mapping library that provides local data persistence with minimal boilerplate code. At compile time, it validates each query against the schema, so that broken SQL queries result in compile time errors instead of runtime failures. Room abstracts away some of the underlying implementation details of working with raw SQL tables and queries. It also allows observing changes to the database data (including collections and join queries), exposing such changes via LiveData objects. In addition, it explicitly defines thread constraints that address common issues such as accessing storage on the main thread.
簡(jiǎn)略一下就是:Room是一個(gè)對(duì)象映射庫磷瘤,(姑且按GreenDAO的功能來理解吧)可以在在編譯的時(shí)候就能檢測(cè)出SQL語句的異常。還能夠在數(shù)據(jù)庫內(nèi)容發(fā)生改變時(shí)通過LiveData的形式發(fā)出通知(想想這有什么用搜变?一個(gè)例子就是通知列表采缚,比如App調(diào)某個(gè)接口可以返回消息通知,App需要把返回的數(shù)據(jù)存儲(chǔ)在本地挠他,方便以后查看扳抽。那么問題來了,在顯示服務(wù)器返回的數(shù)據(jù)之前殖侵,還需要把之前的數(shù)據(jù)展示在界面上贸呢。之前用過的做法是先查數(shù)據(jù)庫展示->請(qǐng)求網(wǎng)絡(luò)->把網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)插到最前面->把網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫中,光是看這個(gè)做法就知道這里的代碼多到想吐拢军,用Room就可以很好的解決這個(gè)問題楞陷,當(dāng)然其他ORM框架有類似的解決方案。Room可以使過程大大簡(jiǎn)化茉唉,查詢顯示->請(qǐng)求網(wǎng)絡(luò)->本地存儲(chǔ)->收到改變通知固蛾,顯示新的數(shù)據(jù)(自動(dòng)發(fā)生,不需要人為操作)度陆。)
那么來看看如何使用Room吧艾凯。以上面用戶信息為例:
- 定義
User
類,并且使用@Entity注解:
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
- 創(chuàng)建RoomDatabase:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
- 創(chuàng)建DAO
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
- 在RoomDatabase中訪問DAO
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
注意:UserDao
中load()
方法返回的是LiveData<User>
坚芜。這就是為什么Room能夠在數(shù)據(jù)庫內(nèi)容發(fā)生改變的時(shí)候通知對(duì)數(shù)據(jù)庫內(nèi)容感興趣的類览芳。
現(xiàn)在可以把Room和UserRepository
結(jié)合起來:
@Singleton
public class UserRepository {
private final Webservice webservice;
private final UserDao userDao;
private final Executor executor;
@Inject
public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
this.webservice = webservice;
this.userDao = userDao;
this.executor = executor;
}
public LiveData<User> getUser(String userId) {
refreshUser(userId);
// return a LiveData directly from the database.(從數(shù)據(jù)庫中直接返回LiveData)
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// running in a background thread(在后臺(tái)線程運(yùn)行)
// check if user was fetched recently(檢查數(shù)據(jù)庫中是否有數(shù)據(jù))
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// refresh the data
Response response = webservice.getUser(userId).execute();
// TODO check for error etc.
// Update the database.The LiveData will automatically refresh so
// we don't need to do anything else here besides updating the database(不用其他代碼就能實(shí)現(xiàn)數(shù)據(jù)自動(dòng)更新)
userDao.save(response.body());
}
});
}
}
從上面的代碼可以看出,即使是改變了UserRepository
中的數(shù)據(jù)來源鸿竖,也無需對(duì)UserProfileViewModel
或者UserProfileFragment
作任何更改沧竟。這就是抽象出了數(shù)據(jù)層的靈活性。
現(xiàn)在基本上就解決之前提到的一些問題缚忧。如果已經(jīng)查看過某個(gè)用戶的數(shù)據(jù)悟泵,那么再次打開這個(gè)用戶的頁面時(shí),數(shù)據(jù)可以瞬間被展示闪水。如果數(shù)據(jù)過期的話糕非,還可以立刻去更新數(shù)據(jù)。當(dāng)然球榆,如果你規(guī)定了數(shù)據(jù)過期就不允許展示朽肥,也可以展示從網(wǎng)絡(luò)獲取的數(shù)據(jù)。
在某些情況下持钉,需要顯示是否本次網(wǎng)絡(luò)請(qǐng)求的進(jìn)度(是否完成)衡招,比如下拉刷新,需要根據(jù)網(wǎng)絡(luò)請(qǐng)求的進(jìn)度展示相應(yīng)的UI每强。最好把UI顯示和實(shí)際的數(shù)據(jù)來源分開始腾,因?yàn)閿?shù)據(jù)可能會(huì)因?yàn)楦鞣N原因被更新州刽。(實(shí)際上我也沒有很好的理解這段話,下面貼下這段話的原文:In some use cases, such as pull-to-refresh, it is important for the UI to show the user if there is currently a network operation in progress. It is good practice to separate the UI action from the actual data since it might be updated for various reasons (for example, if we fetch a list of friends, the same user might be fetched again triggering a LiveData<User> update). From the UI's perspective, the fact that there is a request in flight is just another data point, similar to any other piece data (like the User object).)
數(shù)據(jù)來源的唯一性
在上面提供的代碼中浪箭,數(shù)據(jù)庫是App數(shù)據(jù)的唯一來源穗椅。Google推薦采用這種方式。
最終的架構(gòu)形態(tài)
下面這張圖展示了使用Android Architecture Components來構(gòu)建的App整體的架構(gòu):
一些App架構(gòu)設(shè)計(jì)的推薦準(zhǔn)則
- 不要把在Manifest中定義的組件作為提供數(shù)據(jù)的來源(包括Activity奶栖、Services匹表、Broadcast Receivers等),因?yàn)樗麄兊纳芷谙鄬?duì)于App的生命周期是相對(duì)短暫的驼抹。
- 嚴(yán)格的限制每個(gè)模塊的功能桑孩。比如上面提到的不要再ViewModel中增加如何獲取數(shù)據(jù)的代碼。
- 每個(gè)模塊盡可能少的對(duì)外暴露方法框冀。
- 模塊中對(duì)外暴露的方法要考慮單元測(cè)試的方便。
- 不要重復(fù)造輪子敏簿,把精力放在能夠讓App脫穎而出的業(yè)務(wù)上明也。
- 盡可能多的持久化數(shù)據(jù),因?yàn)檫@樣即使是在網(wǎng)絡(luò)條件不好的情況下惯裕,用戶仍然能夠使用App
- 保證數(shù)據(jù)來源的唯一性(即:提供一個(gè)類似
UserRepository
的類)
附加建議:在網(wǎng)絡(luò)請(qǐng)求過程中提供網(wǎng)絡(luò)請(qǐng)求的狀態(tài)
在上面的文章中温数,刻意忽略了網(wǎng)絡(luò)錯(cuò)誤和處于loading狀態(tài)的處理。下面的代碼將會(huì)提供一個(gè)包含網(wǎng)絡(luò)狀態(tài)的示例代碼蜻势。
//a generic class that describes a data with a status
public class Resource<T> {
@NonNull public final Status status;
@Nullable public final T data;
@Nullable public final String message;
private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
this.status = status;
this.data = data;
this.message = message;
}
public static <T> Resource<T> success(@NonNull T data) {
return new Resource<>(SUCCESS, data, null);
}
public static <T> Resource<T> error(String msg, @Nullable T data) {
return new Resource<>(ERROR, data, msg);
}
public static <T> Resource<T> loading(@Nullable T data) {
return new Resource<>(LOADING, data, null);
}
}
下面這幅圖展示請(qǐng)求數(shù)據(jù)的通用流程
總體的思路就是:請(qǐng)求數(shù)據(jù)之前先查看本地有沒有數(shù)據(jù)撑刺,再根據(jù)本地?cái)?shù)據(jù)的狀態(tài)決定是否進(jìn)行網(wǎng)絡(luò)操作。網(wǎng)絡(luò)操作回來的數(shù)據(jù)緩存在本地握玛。
根據(jù)流程圖的思路可以抽象出NetworkBoundResource
這個(gè)類:
// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
// Called to save the result of the API response into the database(存儲(chǔ)網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù))
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// Called with the data in the database to decide whether it should be
// fetched from the network.(根據(jù)數(shù)據(jù)庫檢索的結(jié)果決定是否需要從網(wǎng)絡(luò)獲取數(shù)據(jù))
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// Called to get the cached data from the database(從數(shù)據(jù)中獲取數(shù)據(jù))
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// Called to create the API call.(創(chuàng)建API)
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// Called when the fetch fails. The child class may want to reset components
// like rate limiter.(獲取數(shù)據(jù)失敗回調(diào))
@MainThread
protected void onFetchFailed() {
}
// returns a LiveData that represents the resource, implemented
// in the base class.(獲取LiveData形式的數(shù)據(jù))
public final LiveData<Resource<ResultType>> getAsLiveData();
}
和數(shù)據(jù)獲取相關(guān)的類只要繼承這個(gè)類够傍,就能按照上面流程圖高效獲取數(shù)據(jù)。值得注意的是NetworkBoundResource<ResultType, RequestType>
有兩種數(shù)據(jù)類型挠铲,這是因?yàn)榉?wù)端返回的數(shù)據(jù)類型和本地存儲(chǔ)的數(shù)據(jù)類型可能是不一致的冕屯。(再來一個(gè)不一定合理的例子,以用戶信息為例拂苹,服務(wù)器為了擴(kuò)展考慮返回的User會(huì)包含一個(gè)年齡字段安聘,但是本地對(duì)這個(gè)字段并沒有任何用處,就沒有必要存儲(chǔ)瓢棒,這樣一來就會(huì)出現(xiàn)不一致的情況)
代碼中的ApiResponse
是對(duì)Retrofit2.Call
的包裝浴韭,主要的作用是把call類型轉(zhuǎn)換成LiveData。
下面是NetworkBoundResource
的詳細(xì)實(shí)現(xiàn):
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
result.setValue(Resource.loading(null));
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldFetch(data)) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource,
newData -> result.setValue(Resource.success(newData)));
}
});
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
LiveData<ApiResponse<RequestType>> apiResponse = createCall();
// we re-attach dbSource as a new source,
// it will dispatch its latest value quickly
result.addSource(dbSource,
newData -> result.setValue(Resource.loading(newData)));
result.addSource(apiResponse, response -> {
result.removeSource(apiResponse);
result.removeSource(dbSource);
//noinspection ConstantConditions
if (response.isSuccessful()) {
saveResultAndReInit(response);
} else {
onFetchFailed();
result.addSource(dbSource,
newData -> result.setValue(
Resource.error(response.errorMessage, newData)));
}
});
}
@MainThread
private void saveResultAndReInit(ApiResponse<RequestType> response) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
saveCallResult(response.body);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
現(xiàn)在我們就可以在UserRepository
里通過NetworkBoundResource
來獲取并且緩存User信息:
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData<Resource<User>> loadUser(final String userId) {
return new NetworkBoundResource<User,User>() {
@Override
protected void saveCallResult(@NonNull User item) {
userDao.insert(item);
}
@Override
protected boolean shouldFetch(@Nullable User data) {
return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
}
@NonNull @Override
protected LiveData<User> loadFromDb() {
return userDao.load(userId);
}
@NonNull @Override
protected LiveData<ApiResponse<User>> createCall() {
return webservice.getUser(userId);
}
}.getAsLiveData();
}
}
通過上面的文章脯宿,大致可以建立起對(duì)整個(gè)Android Architecture Components的認(rèn)識(shí)念颈,同時(shí)也能窺見Google在設(shè)計(jì)這套架構(gòu)后面的思想。整體介紹的文章已經(jīng)結(jié)束了嗅绰,在接下來的文章中舍肠,會(huì)詳細(xì)的介紹涉及的各個(gè)類搀继。
相關(guān)文章:
理解Android Architecture Components系列(一)
理解Android Architecture Components系列(二)
理解Android Architecture Components系列之Lifecycle(三)
理解Android Architecture Components系列之LiveData(四)
理解Android Architecture Components系列之ViewModel(五)
理解Android Architecture Components系列之Room(六)
理解Android Architecture Components系列之Paging Library(七)
理解Android Architecture Components系列之WorkManager(八)