近,官方推出了一份關于應用架構的佳實踐指南。這里就給大家簡要介紹一下:
首先,Android 開發者肯定都知道 Android 中有四大組件,這些組件都有各自的生命周期并且在一定程度上是不受你控制的。在任何時候,Android 操作系統都可能根據用戶的行為或資源緊張等原因回收掉這些組件。
這也就引出了條準則:「不要在應用程序組件中保存任何應用數據或狀態,并且組件間也不應該相互依賴」。
常見的錯誤就是在 Activity 或 Fragment 中寫了與 UI 和交互無關的代碼。盡可能減少對它們的依賴,這能避免大量生命周期導致的問題,以提供更好的用戶體驗。
第二條準則:「通過 model 驅動應用 UI,并盡可能的持久化」。
這樣做主要有兩個原因:
在這里,官方演示了通過使用新推出的 Architecture Components 來構建一個應用。
想象一下,您正在打算開發一個顯示用戶個人信息的界面,用戶數據通過 REST API 從后端獲取。
首先,我們需要創建三個文件:
為了簡單起見,我們這里就省略掉布局文件。
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);
}
}
注意其中的 ViewModel 和 LifecycleFragment 都是 Android 新引入的,可以參考官方說明進行集成。
現在,我們完成了這三個模塊,該如何將它們聯系起來呢?也就是當 ViewModel 中的用戶字段被設置時,我們需要一種方法來通知 UI。這就是 LiveData 的用武之地了。
LiveData 是一個可被觀察的數據持有者(用到了觀察者模式)。其能夠允許 Activity, Fragment 等應用程序組件對其進行觀察,并且不會在它們之間創建強依賴。LiveData 還能夠自動響應各組件的聲明周期事件,防止內存泄漏,從而使應用程序不會消耗更多的內存。
注意: LiveData 和 RxJava 或 Agera 的區別主要在于 LiveData 自動幫助處理了生命周期事件,避免了內存泄漏。
所以,現在我們來修改一下 UserProfileViewModel:
public class UserProfileViewModel extends ViewModel { ... private LiveData<User> user; public LiveData<User> getUser() { return user;
}
}
再在 UserProfileFragment 中對其進行觀察并更新我們的 UI:
@Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> { // update UI });
}
獲取數據
現在,我們聯系了 ViewModel 和 Fragment,但 ViewModel 又怎么來獲取到數據呢?
在這個示例中,我們假定后端提供了 REST API,因此我們選用 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);
}
不要通過 ViewModel 直接來獲取數據,這里我們將工作轉交給一個新的 Repository 模塊。
Repository 模塊負責數據處理,為應用的其他部分提供干凈可靠的 API。你可以將其考慮為不同數據源(Web,緩存或數據庫)與應用之間的中間層。
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;
}
}
管理組件間的依賴關系
根據上面的代碼,我們可以看到 UserRepository 中有一個 Webservice 的實例,不要直接在 UserRepository 中 new 一個 Webservice。這很容易導致代碼的重復與復雜化,比如 UserRepository 很可能不是用到 Webservice 的類,如果每個用到的類都新建一個 Webservice,這顯示會導致資源的浪費。
這里,我們推薦使用 Dagger 2 來管理這些依賴關系。
現在,讓我們來把 ViewModel 和 Repository 連接起來吧:
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;
}
}
緩存數據
在實際項目中,Repository 往往不會只有一個數據源。因此,我們這里在其中再加入緩存:
@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;
}
}
持久化數據
現在當用戶旋轉屏幕或暫時離開應用再回來時,數據是直接可見的,因為是直接從緩存中獲取的數據。但要是用戶長時間關閉應用,并且 Android 還徹底殺死了進程呢?
我們目前的實現中,會再次從網絡中獲取數據。這可不是一個好的用戶體驗。這時就需要數據持久化了。繼續引入一個新組件Room。
Room 能幫助我們方便的實現本地數據持久化,抽象出了很多常用的數據庫操作,并且在編譯時會驗證每個查詢,從而損壞的 SQL 查詢只會導致編譯時錯誤,而不是運行時崩潰。還能和上面介紹的 LiveData 完美合作,并幫開發者處理了很多線程問題。
現在,讓我們來看看怎么使用 Room 吧。: )
首先,在 User 類上面加上 @Entity,將 User 聲明為你數據庫中的一張表。
@Entity class User { @PrimaryKey private int id; private String name; private String lastName; // getters and setters for fields }
再創建數據庫類并繼承 RoomDatabase:
@Database(entities = {User.class}, version = 1) public abstract class MyDatabase extends RoomDatabase { }
注意 MyDatabase 是一個抽象類,Room 會自動添加實現的。
現在我們需要一種方法來將用戶數據插入到數據庫:
@Dao public interface UserDao { @Insert(onConflict = REPLACE) void save(User user); @Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
再在數據庫類中加入 DAO:
@Database(entities = {User.class}, version = 1) public abstract class MyDatabase extends RoomDatabase { public abstract UserDao userDao();
}
注意上面的 load 方法返回的是 LiveData,Room 會知道什么時候數據庫發生了變化并自動通知所有的觀察者。這也就是 LiveData 和 Room 搭配的妙用。
現在繼續修改 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 中的數據源,我們也完全不需要修改 ViewModel 和 Fragment,這就是抽象的好處。同時還非常適合測試,我們可以在測試 UserProfileViewModel 時提供測試用的 UserRepository。
下面部分的內容在原文中是作為附錄,但我個人覺得也很重要,所以擅自挪上來,一起為大家介紹了。: )
在上面的例子中,有心的大家可能發現了我們沒有處理網絡錯誤和正在加載狀態。但在實際開發中其實是很重要的。這里,我們就實現一個工具類來根據不同的網絡狀況選擇不同的數據源。
首先,實現一個 Resource 類:
//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);
}
}
因為,從網絡加載數據和從磁盤加載是很相似的,所以再新建一個 NetworkBoundResource 類,方便多處復用。下面是 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;
}
}
注意上面使用了 ApiResponse 作為網絡請求, ApiResponse 是對于 Retrofit2.Call 的簡單包裝,用于將其響應轉換為 LiveData。
下面是具體的實現:
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();
}
}
現在,我們就能使用 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();
}
}
到這里,我們的代碼就全部完成了。后的架構看起來就像這樣:
下面的原則雖然不是強制性的,但根據我們的經驗遵循它們能使您的代碼更健壯、可測試和可維護的。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。