此指南適用于那些曾經(jīng)或現(xiàn)在進行Android應用的基礎(chǔ)開發(fā)矾睦,并希望了解和學習編寫Android程序的最佳實踐和架構(gòu)。通過學習來構(gòu)建強大的生產(chǎn)級別的應用。
注意:此指南默認你對Android開發(fā)有比較深的理解固翰,熟知Android Framework。如果你還只是個Android開發(fā)新手羹呵,那么建議先學習下Android的基礎(chǔ)知識骂际。
Android程序員面臨的問題
傳統(tǒng)的桌面應用程序開發(fā)在大多數(shù)情況下,啟動器快捷方式都有一個入口點冈欢,并作為一個單一的過程運行歉铝,但Android應用程序的結(jié)構(gòu)更為復雜。典型的Android應用程序由多個應用程序組件構(gòu)成涛癌,包括Activity犯戏,F(xiàn)ragment送火,Service拳话,ContentProvider和Broadcast Receiver。
大多數(shù)這些應用程序組件在Android操作系統(tǒng)使用的AndroidManifest中聲明种吸,以決定如何將應用程序集成到設(shè)備上來為用戶提供完整的體驗弃衍。盡管如前所述瘩蚪,桌面應用程序傳統(tǒng)上是作為一個單一的進程運行的证鸥,但正確編寫的Android應用程序則需要更靈活,因為用戶通過設(shè)備上的不同應用程序編織方式揩页,不斷切換流程和任務猖败。
舉個例子速缆,當用戶在社交App上打算分享一張照片,那么Android系統(tǒng)就會為此啟動相機來完成此次請求恩闻。此時用戶離開了社交App艺糜,但是這個用戶體驗是無縫連接的。相機可能又會觸發(fā)并啟動文件管理器來選擇照片。最終回到社交App并分享照片破停。此外翅楼,在此過程中的任何時候,用戶可能會被打電話中斷真慢,并在完成電話后再回來分享照片毅臊。
在Android中,這種應用間跳轉(zhuǎn)行為很常見黑界,因此你的應用必須正確處理這些流程管嬉。請記住,移動設(shè)備是資源有限的朗鸠,所以在任何時候宠蚂,操作系統(tǒng)可能需要殺死一些應用來為新的應用騰出空間。
你的應用程序的所有組件都可以被單獨啟動或無序啟動童社,并且在任何時候由用戶或系統(tǒng)銷毀求厕。因為應用程序組件是短暫的,它們的生命周期(創(chuàng)建和銷毀時)不受你的控制扰楼,因此你不應該將任何應用程序數(shù)據(jù)或狀態(tài)存儲在應用程序組件中呀癣,并且應用程序組件不應相互依賴。
常見的架構(gòu)原理
如果你無法使用應用程序組件來存儲應用程序數(shù)據(jù)和狀態(tài)弦赖,應如何構(gòu)建應用程序项栏?
在你的App開發(fā)中你應該將重心放在分層上,如果將所有的代碼都寫在Activity或者Fragment中蹬竖,那問題就大了沼沈。任何不是處理UI或跟操作系統(tǒng)交互的操作不應該放在這兩個類中。盡量保持它們代碼的精簡币厕,這樣你可以避免很多與生命周期相關(guān)的問題列另。記住你并不能掌控Activity和Fragment,他們只是在你的App和Android系統(tǒng)間起了橋梁的作用旦装。任何時候页衙,Android系統(tǒng)可能會根據(jù)用戶操作或其他因素(如低內(nèi)存)來回收它們。最好盡量減少對他們的依賴阴绢,以提供堅實的用戶體驗店乐。
還有一點比較重要的就是持久模型驅(qū)動UI。使用持久模型主要是因為當你的UI被回收或者在沒有網(wǎng)絡(luò)的情況下還能正常給用戶展示數(shù)據(jù)呻袭。模型
是用來處理應用數(shù)據(jù)的組件眨八,它們獨立于應用中的視圖和四大組件。因此模型的生命周期必然和UI是分離的左电。保持UI代碼的整潔廉侧,會讓你能更容易的管理和調(diào)整UI含长。讓你的應用基于模型開發(fā)可以很好的管理你應用的數(shù)據(jù)并是你的應用更具測試性和持續(xù)性。
應用架構(gòu)推薦
回到這篇文章的主題伏穆,來說說Android官方架構(gòu)組件(一下簡稱架構(gòu))拘泞。一下會介紹如何在你的應用中實踐這一架構(gòu)模式。
注意:不可能存在某一種架構(gòu)方式可以完美適合任何場景枕扫。話雖如此陪腌,這種架構(gòu)應該是大多數(shù)用例的良好起點。如果你已經(jīng)有了很好的Android應用程序架構(gòu)方式烟瞧,請繼續(xù)保持诗鸭。
假設(shè)我們需要一個現(xiàn)實用戶資料的UI,該用戶的資料文件將使用REST API從服務端獲取参滴。
構(gòu)建用戶界面
我們的這個用戶界面由一個UserProfileFragment.java
文件和它的布局文件user_profile_layout.xml
强岸。
為了驅(qū)動UI,數(shù)據(jù)模型需要持有下面兩個數(shù)據(jù):
- User ID:用戶的標識符砾赔。最好使用Fragment的參數(shù)將此信息傳遞到Fragment中蝌箍。如果Android操作系統(tǒng)回收了Fragment,則會保留此信息暴心,以便下次重新啟動應用時妓盲,該ID可用。
- User Object:傳統(tǒng)的Java對象专普,代表用戶的數(shù)據(jù)悯衬。
為此,我們新建一個繼承自ViewModel的名為UserProfileViewModel
的模型來持有這個數(shù)據(jù)檀夹。
ViewModel提供特定UI組件的數(shù)據(jù)筋粗,例如Activity和Fragment,并處理與數(shù)據(jù)處理業(yè)務部分的通信炸渡,例如調(diào)用其他組件來加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶修改娜亿。ViewModel不了解View,并且不受UI的重建(如重由于旋轉(zhuǎn)而導致的Activity的重建)的影響偶摔。
現(xiàn)在我們有一下三個文件:
- user_profile.xml: 視圖的布局文件暇唾。
- UserProfileViewModel.java: 持有UI數(shù)據(jù)的模型。
- UserProfileFragment.java: 用于顯示數(shù)據(jù)模型中的數(shù)據(jù)并和用戶進行交互辰斋。
一下是具體代碼(為了簡化,布局文件省略)瘸味。
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
public class UserProfileFragment extends LifecycleFragment {
private static final String UID_KEY = "uid";
private UserProfileViewModel viewModel;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String userId = getArguments().getString(UID_KEY);
viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
viewModel.init(userId);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.user_profile, container, false);
}
}
注意:上面的UserProfileFragment繼承自LifeCycleFragment而不是Fragment宫仗。當Lifecycle的Api穩(wěn)定后,F(xiàn)ragment會默認實現(xiàn)LifeCycleOwner旁仿。
現(xiàn)在藕夫,我們有三個文件孽糖,我們?nèi)绾芜B接它們?畢竟毅贮,當ViewModel的用戶字段被設(shè)置時办悟,我們需要一種通知UI的方法。這里就要提到LiveData了滩褥。
LiveData是一個可觀察的數(shù)據(jù)持有者病蛉。它允許應用程序中的組件觀察LiveData對象持有的數(shù)據(jù),而不會在它們之間創(chuàng)建顯式和剛性的依賴路徑瑰煎。LiveData還尊重你的應用程序組件(Activity铺然,F(xiàn)ragment,Service)的生命周期狀態(tài)酒甸,并做正確的事情以防止內(nèi)存泄漏魄健,從而你的應用程序不會消耗更多的內(nèi)存。
如果你已經(jīng)使用了想Rxjava活著Agrea這類第三方庫插勤,那么你可以使用它們代替LiveData沽瘦,不過你需要處理好它們與組件生命周期之間的關(guān)系。
現(xiàn)在我們使用LiveData<User>
來代替UserProfileViewModel中的User字段农尖。所以Fragment可以通過觀察它來更新數(shù)據(jù)其垄。LiveData值得稱道的地方就在于它是生命周期感知的,當生命周期結(jié)束是卤橄,其上的觀察者會被即使清理绿满。
public class UserProfileViewModel extends ViewModel {
...
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
然后將UserProfileFragment修改如下,觀察數(shù)據(jù)并更新UI:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// update UI
});
}
一旦用戶數(shù)據(jù)更新窟扑,onChanged回調(diào)將被調(diào)用然后UI會被刷新喇颁。
如果你熟悉一些使用觀察者模式第三方庫,你會覺得奇怪嚎货,為什么沒有在Fragment的onStop()方法中將觀察者移除橘霎。對于LiveData來說這是沒有必要的,因為它是生命周期感知的殖属,這意味著如果UI處于不活動狀態(tài)姐叁,它就不會調(diào)用觀察者的回調(diào)來更新數(shù)據(jù)。并且在onDestroy后會自動移除洗显。
我們也不需要處理任何視圖重建(如屏幕旋轉(zhuǎn))外潜。ViewModel會自動恢復重建前的數(shù)據(jù)。當新的視圖被創(chuàng)建出來后挠唆,它會接收到與之前相同的ViewModel實例处窥,并且觀察者的回調(diào)會被立刻調(diào)用,更新最新的數(shù)據(jù)玄组。這也是ViewModel為什么不能直接引用視圖對象滔驾,因為它的生命周期長于視圖對象谒麦。
獲取數(shù)據(jù)
現(xiàn)在我們將視圖和模型連接起來,但是模型該怎么獲取數(shù)據(jù)呢哆致?在這個例子中绕德,我們假設(shè)使用REST API從后臺獲取。我們將使用Retrofit來向后臺請求數(shù)據(jù)摊阀。
我們的retrofit類Webservice如下:
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);
}
如果只是簡單的實現(xiàn)耻蛇,ViewModel可以直接操作Webservice來獲取用戶數(shù)據(jù)。雖然這樣可以正常工作驹溃,但你的應用無法保證它的后續(xù)迭代城丧。因為這樣做將太多的責任讓ViewModel來承擔,這樣就違反類之前講到的分層原則豌鹤。又因為ViewModel的生命周期是綁定在Activity和Fragment上的亡哄,所以當UI被銷毀后如果丟失所有數(shù)據(jù)將是很差的用戶體驗。所以我們的ViewModel將和一個新的模塊進行交互布疙,這個模塊叫Repository蚊惯。
Repository模塊負責處理數(shù)據(jù)。它為應用程序的其余部分提供了一個干凈的API灵临。他知道在數(shù)據(jù)更新時從哪里獲取數(shù)據(jù)和調(diào)用哪些API調(diào)用截型。你可以將它們視為不同數(shù)據(jù)源(持久性模型,Web服務儒溉,緩存等)之間的中介者宦焦。
UserRepository類如下:
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// This is not an optimal implementation, we'll fix it below
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
data.setValue(response.body());
}
});
return data;
}
}
雖然repository模塊看上去沒有必要,但他起著重要的作用顿涣。它為App的其他部分抽象出了數(shù)據(jù)源〔郑現(xiàn)在我們的ViewModel并不知道數(shù)據(jù)是通過WebService來獲取的,這意味著我們可以隨意替換掉獲取數(shù)據(jù)的實現(xiàn)涛碑。
管理組件間的依賴關(guān)系
上面這種寫法可以看出來UserRepository需要初始化Webservice實例精堕,這雖然說起來簡單,但要實現(xiàn)的話還需要知道Webservice的具體構(gòu)造方法該如何寫蒲障。這將加大代碼的復雜度歹篓,另外UserRepository可能并不是唯一使用Webservice的對象,所以這種在內(nèi)部構(gòu)建Webservice實例顯然是不推薦的揉阎,下面有兩種模式來解決這個問題:
- 依賴注入:依賴注入允許類定義它們的依賴關(guān)系而不構(gòu)造它們庄撮。在運行時,另一個類負責提供這些依賴關(guān)系余黎。我們建議在Android應用程序中使用Google的Dagger 2庫實現(xiàn)依賴注入重窟。Dagger 2通過遍歷依賴關(guān)系樹自動構(gòu)建對象,并在依賴關(guān)系上提供編譯時保證惧财。
- 服務定位器:服務定位器提供了一個注冊表巡扇,其中類可以獲取它們的依賴關(guān)系而不是構(gòu)造它們。與依賴注入(DI)相比垮衷,實現(xiàn)起來相對容易厅翔,因此如果您不熟悉DI,請改用Service Locator搀突。
這些模式允許你擴展代碼刀闷,因為它們提供明確的模式來管理依賴關(guān)系,而不會重復代碼或增加復雜性仰迁。兩者都允許交換實現(xiàn)進行測試;這是使用它們的主要好處之一甸昏。在這個例子中,我們將使用Dagger 2來管理依賴關(guān)系徐许。
連接ViewModel和Repository
現(xiàn)在施蜜,我們的UserProfileViewModel可以改寫成這樣:
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ù)
上面的Repository雖然網(wǎng)絡(luò)請求做了封裝,但是它依賴后臺數(shù)據(jù)源雌隅,所以存在不足翻默。
上面的UserRepository實現(xiàn)的問題是,在獲取數(shù)據(jù)之后恰起,它不會保留在任何地方修械。如果用戶離開UserProfileFragment并重新進來,則應用程序?qū)⒅匦芦@取數(shù)據(jù)检盼。這是不好的肯污,有兩個原因:它浪費了寶貴的網(wǎng)絡(luò)帶寬和迫使用戶等待新的查詢完成。為了解決這個問題吨枉,我們將向我們的UserRepository添加一個新的數(shù)據(jù)源蹦渣,它將把User對象緩存在內(nèi)存中。如下:
@Singleton // informs Dagger that this class should be constructed once
public class UserRepository {
private Webservice webservice;
// simple in memory cache, details omitted for brevity
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ù)
在當前的實現(xiàn)中东羹,如果用戶旋轉(zhuǎn)屏幕或離開并返回到應用程序剂桥,現(xiàn)有UI將立即可見,因為Repository會從內(nèi)存中檢索數(shù)據(jù)属提。但是权逗,如果用戶離開應用程序,并在Android操作系統(tǒng)殺死進程后幾小時后又會怎么樣冤议?
在目前的實現(xiàn)中斟薇,我們將需要從網(wǎng)絡(luò)中再次獲取數(shù)據(jù)。這不僅是一個糟糕的用戶體驗恕酸,也是浪費堪滨,因為它將使用移動數(shù)據(jù)來重新獲取相同的數(shù)據(jù)。你以通過緩存Web請求來簡單地解決這個問題蕊温,但它會產(chǎn)生新的問題袱箱。如果請求一個朋友列表而不是單個用戶遏乔,會發(fā)生什么情況?那么你的應用程序可能會顯示不一致的數(shù)據(jù)发笔,這是最令人困惑的用戶體驗盟萨。例如,相同的用戶的數(shù)據(jù)可能會不同了讨,因為朋友列表請求和用戶請求可以在不同的時間執(zhí)行捻激。你的應用需要合并他們,以避免顯示不一致的數(shù)據(jù)前计。
正確的處理方法是使用持久模型胞谭。這時候Room就派上用場了。
Room是一個對象映射庫男杈,它提供本地數(shù)據(jù)持久性和最少的樣板代碼丈屹。在編譯時,它根據(jù)模式驗證每個查詢势就,從而錯誤的SQL查詢會導致編譯時錯誤泉瞻,而不是運行時失敗。Room抽象了使用原始SQL表和查詢的一些基本實現(xiàn)細節(jié)苞冯。它還允許觀察數(shù)據(jù)庫數(shù)據(jù)(包括集合和連接查詢)的更改袖牙,通過LiveData對象公開這些更改。
要使用Room我們首先需要使用@Entity來定義實體:
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
接著創(chuàng)建數(shù)據(jù)庫類:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
值得注意的是MyDatabase是一個抽象了舅锄,Room會在編譯期間提供它的一個實現(xiàn)類鞭达。
接下來需要定義DAO:
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
接著在MyDatabase中添加獲取上面這個DAO的方法:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
這里的load方法返回的是LiveData<User>,所以當相關(guān)數(shù)據(jù)庫中的數(shù)據(jù)有任何變化時皇忿,Room都會通知LiveData上的處于活動狀態(tài)的觀察者畴蹭。
現(xiàn)在我們可以修改UserRepository了:
@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.
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// running in a background thread
// check if user was fetched recently
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
userDao.save(response.body());
}
});
}
}
這里雖然我們將UserRepository的直接數(shù)據(jù)來源從Webservice改為本地數(shù)據(jù)庫,但我們卻不需要修改UserProfileViewModel或者UserProfileFragment鳍烁。這就是抽象層帶來的好處叨襟。這也給測試帶來了方便,因為你可以提供一個虛假的UserRepository來測試你的UserProfileViewModel幔荒。
現(xiàn)在糊闽,如果用戶重新回到這個界面,他們會立刻看到數(shù)據(jù)爹梁,因為我們已經(jīng)將數(shù)據(jù)做了持久化的保存右犹。當然如果有用例需要,我們也可不展示太老舊的持久化數(shù)據(jù)姚垃。
在一些用例中念链,比如下拉刷新,如果正處于網(wǎng)絡(luò)請求中,那UI需要告訴用戶正處于網(wǎng)絡(luò)請求中掂墓。一個好的實踐方式就是將UI與數(shù)據(jù)分離谦纱,因為UI可能因為各種原因被更新。從UI的角度來說梆暮,請求中的數(shù)據(jù)和本地數(shù)據(jù)類似服协,只是它還沒有被持久化到數(shù)據(jù)庫中绍昂。
以下有兩種解決方法:
- 將getUser的返回值中加入網(wǎng)絡(luò)狀態(tài)啦粹。
- 在Repository中提供一個可以返回刷新狀態(tài)的方法。如果你只是想在用戶通過下拉刷新來告訴用戶目前的網(wǎng)絡(luò)狀態(tài)的話窘游,那這個方法是比較適合的唠椭。
數(shù)據(jù)唯一來源
在以上實例中,數(shù)據(jù)唯一來源是數(shù)據(jù)庫忍饰,這樣做的好處是用戶可以基于穩(wěn)定的數(shù)據(jù)庫數(shù)據(jù)來更新頁面贪嫂,而不需要處理大量的網(wǎng)絡(luò)請求狀態(tài)。數(shù)據(jù)庫有數(shù)據(jù)則使用艾蓝,沒有數(shù)據(jù)則等待其更新力崇。
測試
我們之前提到分層可以個應用提供良好的測試能力,接下來就看看我們怎么測試不同的模塊赢织。
-
用戶界面與交互:這是唯一一個需要使用到
Android UI Instrumentation test
的測試模塊亮靴。測試UI的最好方法就是使用Espresso框架。你可以創(chuàng)建Fragment然后提供一個虛假的ViewModel于置。因為Fragment只跟ViewModel交互茧吊,所以虛擬一個ViewModel就足夠了。 - ViewModel:ViewModel可以用JUnit test進行測試八毯。因為其不涉及界面與交互搓侄。而且你只需要虛擬UserRepository即可。
- UserRepository:測試UserRepository同樣使用JUnit test话速。你可以虛擬出Webservice和DAO讶踪。你可以通過使用正確的網(wǎng)絡(luò)請求來請求數(shù)據(jù),讓后將數(shù)據(jù)通過DAO寫入數(shù)據(jù)庫泊交。如果數(shù)據(jù)庫中有相關(guān)數(shù)據(jù)則無需進行網(wǎng)絡(luò)請求乳讥。
- UserDao:對于DAO的測試,推薦使用instrumentation進行測試活合。因為此處無需UI雏婶,并且可以使用in-memory數(shù)據(jù)庫來保證測試的封閉性,不會影響到磁盤上的數(shù)據(jù)庫白指。
- Webservice:保持測試的封閉性是相當重要的留晚,因此即使是你的Webservice測試也應避免對后端進行網(wǎng)絡(luò)呼叫。有很多第三方庫提供這方面的支持。例如错维,MockWebServer是一個很棒的庫奖地,可以幫助你為你的測試創(chuàng)建一個假的本地服務器。
架構(gòu)圖
指導原則
編程是一個創(chuàng)意領(lǐng)域赋焕,構(gòu)建Android應用程序也不例外参歹。有多種方法來解決問題,無論是在多個Activity或Fragment之間傳遞數(shù)據(jù)隆判,還是檢索遠程數(shù)據(jù)并將其在本地保持離線模式犬庇,或者是任何其他常見的場景。
雖然以下建議不是強制性的侨嘀,但經(jīng)驗告訴我們臭挽,遵循這些建議將使你的代碼庫從長遠來看更加強大,可測試和可維護咬腕。
- 在AndroidManifest中定義的Activity欢峰,Service,Broadcast Receiver等涨共,它們不是數(shù)據(jù)源纽帖。相反,他們只是用于協(xié)調(diào)和展示數(shù)據(jù)举反。由于每個應用程序組件的壽命相當短懊直,運行狀態(tài)取決于用戶與其設(shè)備的交互以及運行時的整體當前運行狀況,所以不要將這些組件作為數(shù)據(jù)源照筑。
- 你需要在應用程序的各個模塊之間創(chuàng)建明確界定的責任范圍吹截。例如,不要在不同的類或包之間傳遞用于加載網(wǎng)絡(luò)數(shù)據(jù)的代碼凝危。同樣波俄,不要將數(shù)據(jù)緩存和數(shù)據(jù)綁定這兩個責任完全不同的放在同一個類中。
- 每個模塊之間要竟可能少的相互暴露蛾默。不要抱有僥幸心理去公開一個關(guān)于模塊的內(nèi)部實現(xiàn)細節(jié)的接口懦铺。你可能會在短期內(nèi)獲得到便捷,但是隨著代碼庫的發(fā)展支鸡,你將多付多次技術(shù)性債務冬念。
- 當你定義模塊之間的交互時,請考慮如何使每個模塊隔離牧挣。例如急前,擁有用于從網(wǎng)絡(luò)中提取數(shù)據(jù)的定義良好的API將使得更容易測試在本地數(shù)據(jù)庫中持久存在該數(shù)據(jù)的模塊。相反瀑构,如果將這兩個模塊的邏輯組合在一起裆针,或者將整個代碼庫中的網(wǎng)絡(luò)代碼放在一起,那么測試就更難(如果不是不可能)。
- 你的應用程序的核心是什么讓它獨立出來世吨。不要花時間重復輪子或一次又一次地編寫相同的樣板代碼澡刹。相反,將精力集中在使你的應用程序獨一無二的同時耘婚,讓Android架構(gòu)組件和其他推薦的庫來處理重復的樣板代碼罢浇。
- 保持盡可能多的相關(guān)聯(lián)的新鮮數(shù)據(jù),以便你的應用程序在設(shè)備處于脫機模式時可用沐祷。雖然你可以享受恒定和高速連接嚷闭,但你的用戶可能不會。
- 你的Repository應指定一個數(shù)據(jù)源作為真實的單一來源戈轿。每當你的應用程序需要訪問這些數(shù)據(jù)時凌受,它應該始終源于真實的單一來源。
擴展: 公開網(wǎng)絡(luò)狀態(tài)
在上面的小結(jié)我們故意省略了網(wǎng)絡(luò)錯誤和加載狀態(tài)來保證例子的簡潔性思杯。在這一小結(jié)我們演示一種使用Resource類來封裝數(shù)據(jù)及其狀態(tài)。以此來公開網(wǎng)絡(luò)狀態(tài)挠进。
下面是簡單的Resource實現(xiàn):
//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);
}
}
以為從網(wǎng)絡(luò)上抓取視頻的同時在UI上顯示數(shù)據(jù)庫的舊數(shù)據(jù)是很常見的用例色乾,所以我們要創(chuàng)建一個可以在多個地方重復使用的幫助類NetworkBoundResource。以下是NetworkBoundResource的決策樹:
NetworkBoundResource從觀察數(shù)據(jù)庫開始领突,當?shù)谝淮螐臄?shù)據(jù)庫加載完實體后暖璧,NetworkBoundResource會檢查這個結(jié)果是否滿足用來展示的需求,如不滿足則需要從網(wǎng)上重新獲取君旦。當然以上兩種情況可能同時發(fā)生澎办,你希望先將數(shù)據(jù)顯示在UI上的同時去網(wǎng)絡(luò)上請求新數(shù)據(jù)。
如果網(wǎng)絡(luò)請求成果金砍,則將結(jié)果保存到數(shù)據(jù)庫局蚀,然后重新從數(shù)據(jù)庫加載數(shù)據(jù),如果網(wǎng)絡(luò)請求失敗恕稠,則直接傳遞錯誤信息琅绅。
注意:在上面的過程中可以看到當將新數(shù)據(jù)保存到數(shù)據(jù)庫后,我們重新從數(shù)據(jù)庫加載數(shù)據(jù)鹅巍。雖然大部分情況我們不必如此千扶,因為數(shù)據(jù)庫會為我們傳遞此次更新。但另一方面骆捧,依賴數(shù)據(jù)庫內(nèi)部的更新機制并不是我們想要的如果更新的數(shù)據(jù)與舊數(shù)據(jù)一致澎羞,則數(shù)據(jù)谷不會做出更新提示。我們也不希望直接從網(wǎng)絡(luò)請求中獲取數(shù)據(jù)直接用于UI敛苇,因為這樣違背了單一數(shù)據(jù)源的原則妆绞。
下面是NetworkBoundResource類的公共api:
// 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
@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.
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// Called to get the cached data from the database
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// Called to create the API call.
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// Called when the fetch fails. The child class may want to reset components
// like rate limiter.
@MainThread
protected void onFetchFailed() {
}
// returns a LiveData that represents the resource
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
注意到上面定義了兩種泛型,ResultType和RequestType,因為從網(wǎng)絡(luò)請求返回的數(shù)據(jù)類型可能會和數(shù)據(jù)庫返回的不一致摆碉。
另外注意到上面代碼中的ApiResponse這個類塘匣,他是將Retroft2.Call轉(zhuǎn)換成LiveData的一個簡單封裝。
下面是NetworkBoundResource余下部分的實現(xiàn):
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
//1初始化NetworkBoundResource
result.setValue(Resource.loading(null));
//2從數(shù)據(jù)庫加載本地數(shù)據(jù)
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
//3加載完成后判斷是否需要從網(wǎng)上更新數(shù)據(jù)
result.removeSource(dbSource);
if (shouldFetch(data)) {
//4從網(wǎng)上更新數(shù)據(jù)
fetchFromNetwork(dbSource);
} else {
//直接用本地數(shù)據(jù)更新
result.addSource(dbSource,
newData -> result.setValue(Resource.success(newData)));
}
});
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
//5進行網(wǎng)絡(luò)請求
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()) {
//6請求數(shù)據(jù)成功巷帝,保存數(shù)據(jù)
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) {
//7保存請求到的數(shù)據(jù)
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.
//8再次加載數(shù)據(jù)庫,使用數(shù)據(jù)庫中的最新數(shù)據(jù)
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
}
接著我們就可以在UserRepository中使用NetworkBoundResource了楞泼。
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();
}
}