最近,官方推出了一份關(guān)于應(yīng)用架構(gòu)的最佳實(shí)踐指南。這里就給大家簡要介紹一下: 

首先,Android 開發(fā)者肯定都知道 Android 中有四大組件,這些組件都有各自的生命周期并且在一定程度上是不受你控制的。在任何時(shí)候,Android 操作系統(tǒng)都可能根據(jù)用戶的行為或資源緊張等原因回收掉這些組件。 

這也就引出了第一條準(zhǔn)則:「不要在應(yīng)用程序組件中保存任何應(yīng)用數(shù)據(jù)或狀態(tài),并且組件間也不應(yīng)該相互依賴」。 

最常見的錯(cuò)誤就是在 Activity 或 Fragment 中寫了與 UI 和交互無關(guān)的代碼。盡可能減少對它們的依賴,這能避免大量生命周期導(dǎo)致的問題,以提供更好的用戶體驗(yàn)。 

第二條準(zhǔn)則:「通過 model 驅(qū)動應(yīng)用 UI,并盡可能的持久化」。 
這樣做主要有兩個(gè)原因: 

  • 如果系統(tǒng)回收了你的應(yīng)用資源或其他什么意外情況,不會導(dǎo)致用戶丟失數(shù)據(jù)。

  • Model 就應(yīng)該是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件。獨(dú)立于視圖和應(yīng)用程序組件,保持了視圖代碼的簡單,也讓你的應(yīng)用邏輯更容易管理。并且,將應(yīng)用數(shù)據(jù)置于 model 類中,也更有利于測試。

官方推薦的 App 架構(gòu) 

在這里,官方演示了通過使用最新推出的 Architecture Components 來構(gòu)建一個(gè)應(yīng)用。 

想象一下,您正在打算開發(fā)一個(gè)顯示用戶個(gè)人信息的界面,用戶數(shù)據(jù)通過 REST API 從后端獲取。 

首先,我們需要?jiǎng)?chuàng)建三個(gè)文件: 

  • user_profile.xml:定義界面。

  • UserProfileViewModel.java:數(shù)據(jù)類。

  • UserProfileFragment.java:顯示 ViewModel 中的數(shù)據(jù)并對用戶的交互做出反應(yīng)。


為了簡單起見,我們這里就省略掉布局文件。 

代碼 

  1. public class UserProfileViewModel extends ViewModel {  

  2.     private String userId;  

  3.     private User user;  

  4.   

  5.     public void init(String userId) {  

  6.         this.userId = userId;  

  7.     }  

  8.     public User getUser() {  

  9.         return user;  

  10.     }  

  11. }  


代碼 

  1. public class UserProfileFragment extends LifecycleFragment {  

  2.     private static final String UID_KEY = "uid";  

  3.     private UserProfileViewModel viewModel;  

  4.   

  5.     @Override  

  6.     public void onActivityCreated(@Nullable Bundle savedInstanceState) {  

  7.         super.onActivityCreated(savedInstanceState);  

  8.         String userId = getArguments().getString(UID_KEY);  

  9.         viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);  

  10.         viewModel.init(userId);  

  11.     }  

  12.   

  13.     @Override  

  14.     public View onCreateView(LayoutInflater inflater,  

  15.                 @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {  

  16.         return inflater.inflate(R.layout.user_profile, container, false);  

  17.     }  

  18. }  


注意其中的 ViewModel 和 LifecycleFragment 都是 Android 新引入的,可以參考官方說明進(jìn)行集成。 

現(xiàn)在,我們完成了這三個(gè)模塊,該如何將它們聯(lián)系起來呢?也就是當(dāng) ViewModel 中的用戶字段被設(shè)置時(shí),我們需要一種方法來通知 UI。這就是 LiveData 的用武之地了。 

引用

LiveData 是一個(gè)可被觀察的數(shù)據(jù)持有者(用到了觀察者模式)。其能夠允許 Activity, Fragment 等應(yīng)用程序組件對其進(jìn)行觀察,并且不會在它們之間創(chuàng)建強(qiáng)依賴。LiveData 還能夠自動響應(yīng)各組件的聲明周期事件,防止內(nèi)存泄漏,從而使應(yīng)用程序不會消耗更多的內(nèi)存。 
注意: LiveData 和 RxJava 或 Agera 的區(qū)別主要在于 LiveData 自動幫助處理了生命周期事件,避免了內(nèi)存泄漏。


所以,現(xiàn)在我們來修改一下 UserProfileViewModel: 

代碼 

  1. public class UserProfileViewModel extends ViewModel {  

  2.     ...  

  3.     private LiveData<User> user;  

  4.     public LiveData<User> getUser() {  

  5.         return user;  

  6.     }  

  7. }  


再在 UserProfileFragment 中對其進(jìn)行觀察并更新我們的 UI: 

代碼 

  1. @Override  

  2. public void onActivityCreated(@Nullable Bundle savedInstanceState) {  

  3.     super.onActivityCreated(savedInstanceState);  

  4.     viewModel.getUser().observe(this, user -> {  

  5.       // update UI  

  6.     });  

  7. }  


獲取數(shù)據(jù) 
現(xiàn)在,我們聯(lián)系了 ViewModel 和 Fragment,但 ViewModel 又怎么來獲取到數(shù)據(jù)呢? 

在這個(gè)示例中,我們假定后端提供了 REST API,因此我們選用 Retrofit 來訪問我們的后端。 

首先,定義一個(gè) Webservice: 

代碼 

  1. public interface Webservice {  

  2.     /**  

  3.      * @GET declares an HTTP GET request  

  4.      * @Path("user") annotation on the userId parameter marks it as a  

  5.      * replacement for the {user} placeholder in the @GET path  

  6.      */  

  7.     @GET("/users/{user}")  

  8.     Call<User> getUser(@Path("user") String userId);  

  9. }  


不要通過 ViewModel 直接來獲取數(shù)據(jù),這里我們將工作轉(zhuǎn)交給一個(gè)新的 Repository 模塊。 

引用

Repository 模塊負(fù)責(zé)數(shù)據(jù)處理,為應(yīng)用的其他部分提供干凈可靠的 API。你可以將其考慮為不同數(shù)據(jù)源(Web,緩存或數(shù)據(jù)庫)與應(yīng)用之間的中間層。


代碼 

  1. public class UserRepository {  

  2.     private Webservice webservice;  

  3.     // ...  

  4.     public LiveData<User> getUser(int userId) {  

  5.         // This is not an optimal implementation, we'll fix it below  

  6.         final MutableLiveData<User> data = new MutableLiveData<>();  

  7.         webservice.getUser(userId).enqueue(new Callback<User>() {  

  8.             @Override  

  9.             public void onResponse(Call<User> call, Response<User> response) {  

  10.                 // error case is left out for brevity  

  11.                 data.setValue(response.body());  

  12.             }  

  13.         });  

  14.         return data;  

  15.     }  

  16. }  


管理組件間的依賴關(guān)系 

根據(jù)上面的代碼,我們可以看到 UserRepository 中有一個(gè) Webservice 的實(shí)例,不要直接在 UserRepository 中 new 一個(gè) Webservice。這很容易導(dǎo)致代碼的重復(fù)與復(fù)雜化,比如 UserRepository 很可能不是唯一用到 Webservice 的類,如果每個(gè)用到的類都新建一個(gè) Webservice,這顯示會導(dǎo)致資源的浪費(fèi)。 
這里,我們推薦使用 Dagger 2 來管理這些依賴關(guān)系。 

現(xiàn)在,讓我們來把 ViewModel 和 Repository 連接起來吧: 

代碼 

  1. public class UserProfileViewModel extends ViewModel {  

  2.     private LiveData<User> user;  

  3.     private UserRepository userRepo;  

  4.   

  5.     @Inject // UserRepository parameter is provided by Dagger 2  

  6.     public UserProfileViewModel(UserRepository userRepo) {  

  7.         this.userRepo = userRepo;  

  8.     }  

  9.   

  10.     public void init(String userId) {  

  11.         if (this.user != null) {  

  12.             // ViewModel is created per Fragment so  

  13.             // we know the userId won't change  

  14.             return;  

  15.         }  

  16.         user = userRepo.getUser(userId);  

  17.     }  

  18.   

  19.     public LiveData<User> getUser() {  

  20.         return this.user;  

  21.     }  

  22. }  


緩存數(shù)據(jù) 

在實(shí)際項(xiàng)目中,Repository 往往不會只有一個(gè)數(shù)據(jù)源。因此,我們這里在其中再加入緩存: 

代碼 

  1. @Singleton  // informs Dagger that this class should be constructed once  

  2. public class UserRepository {  

  3.     private Webservice webservice;  

  4.     // simple in memory cache, details omitted for brevity  

  5.     private UserCache userCache;  

  6.     public LiveData<User> getUser(String userId) {  

  7.         LiveData<User> cached = userCache.get(userId);  

  8.         if (cached != null) {  

  9.             return cached;  

  10.         }  

  11.   

  12.         final MutableLiveData<User> data = new MutableLiveData<>();  

  13.         userCache.put(userId, data);  

  14.         // this is still suboptimal but better than before.  

  15.         // a complete implementation must also handle the error cases.  

  16.         webservice.getUser(userId).enqueue(new Callback<User>() {  

  17.             @Override  

  18.             public void onResponse(Call<User> call, Response<User> response) {  

  19.                 data.setValue(response.body());  

  20.             }  

  21.         });  

  22.         return data;  

  23.     }  

  24. }  


持久化數(shù)據(jù) 

現(xiàn)在當(dāng)用戶旋轉(zhuǎn)屏幕或暫時(shí)離開應(yīng)用再回來時(shí),數(shù)據(jù)是直接可見的,因?yàn)槭侵苯訌木彺嬷蝎@取的數(shù)據(jù)。但要是用戶長時(shí)間關(guān)閉應(yīng)用,并且 Android 還徹底殺死了進(jìn)程呢? 

我們目前的實(shí)現(xiàn)中,會再次從網(wǎng)絡(luò)中獲取數(shù)據(jù)。這可不是一個(gè)好的用戶體驗(yàn)。這時(shí)就需要數(shù)據(jù)持久化了。繼續(xù)引入一個(gè)新組件 Room。 

引用

Room 能幫助我們方便的實(shí)現(xiàn)本地?cái)?shù)據(jù)持久化,抽象出了很多常用的數(shù)據(jù)庫操作,并且在編譯時(shí)會驗(yàn)證每個(gè)查詢,從而損壞的 SQL 查詢只會導(dǎo)致編譯時(shí)錯(cuò)誤,而不是運(yùn)行時(shí)崩潰。還能和上面介紹的 LiveData 完美合作,并幫開發(fā)者處理了很多線程問題。


現(xiàn)在,讓我們來看看怎么使用 Room 吧。: ) 

首先,在 User 類上面加上 @Entity,將 User 聲明為你數(shù)據(jù)庫中的一張表。 

代碼 

  1. @Entity  

  2. class User {  

  3.   @PrimaryKey  

  4.   private int id;  

  5.   private String name;  

  6.   private String lastName;  

  7.   // getters and setters for fields  

  8. }  


再創(chuàng)建數(shù)據(jù)庫類并繼承 RoomDatabase: 

代碼 

  1. @Database(entities = {User.class}, version = 1)  

  2. public abstract class MyDatabase extends RoomDatabase {  

  3. }  


注意 MyDatabase 是一個(gè)抽象類,Room 會自動添加實(shí)現(xiàn)的。 

現(xiàn)在我們需要一種方法來將用戶數(shù)據(jù)插入到數(shù)據(jù)庫: 

代碼 

  1. @Dao  

  2. public interface UserDao {  

  3.     @Insert(onConflict = REPLACE)  

  4.     void save(User user);  

  5.     @Query("SELECT * FROM user WHERE id = :userId")  

  6.     LiveData<User> load(String userId);  

  7. }  


再在數(shù)據(jù)庫類中加入 DAO: 

代碼 

  1. @Database(entities = {User.class}, version = 1)  

  2. public abstract class MyDatabase extends RoomDatabase {  

  3.     public abstract UserDao userDao();  

  4. }  


注意上面的 load 方法返回的是 LiveData,Room 會知道什么時(shí)候數(shù)據(jù)庫發(fā)生了變化并自動通知所有的觀察者。這也就是 LiveData 和 Room 搭配的妙用。 

現(xiàn)在繼續(xù)修改 UserRepository: 

代碼 

  1. @Singleton  

  2. public class UserRepository {  

  3.     private final Webservice webservice;  

  4.     private final UserDao userDao;  

  5.     private final Executor executor;  

  6.   

  7.     @Inject  

  8.     public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {  

  9.         this.webservice = webservice;  

  10.         this.userDao = userDao;  

  11.         this.executor = executor;  

  12.     }  

  13.   

  14.     public LiveData<User> getUser(String userId) {  

  15.         refreshUser(userId);  

  16.         // return a LiveData directly from the database.  

  17.         return userDao.load(userId);  

  18.     }  

  19.   

  20.     private void refreshUser(final String userId) {  

  21.         executor.execute(() -> {  

  22.             // running in a background thread  

  23.             // check if user was fetched recently  

  24.             boolean userExists = userDao.hasUser(FRESH_TIMEOUT);  

  25.             if (!userExists) {  

  26.                 // refresh the data  

  27.                 Response response = webservice.getUser(userId).execute();  

  28.                 // TODO check for error etc.  

  29.                 // Update the database.The LiveData will automatically refresh so  

  30.                 // we don't need to do anything else here besides updating the database  

  31.                 userDao.save(response.body());  

  32.             }  

  33.         });  

  34.     }  

  35. }  


可以看到,即使我們更改了 UserRepository 中的數(shù)據(jù)源,我們也完全不需要修改 ViewModel 和 Fragment,這就是抽象的好處。同時(shí)還非常適合測試,我們可以在測試 UserProfileViewModel 時(shí)提供測試用的 UserRepository。 

引用

下面部分的內(nèi)容在原文中是作為附錄,但我個(gè)人覺得也很重要,所以擅自挪上來,一起為大家介紹了。: )


在上面的例子中,有心的大家可能發(fā)現(xiàn)了我們沒有處理網(wǎng)絡(luò)錯(cuò)誤和正在加載狀態(tài)。但在實(shí)際開發(fā)中其實(shí)是很重要的。這里,我們就實(shí)現(xiàn)一個(gè)工具類來根據(jù)不同的網(wǎng)絡(luò)狀況選擇不同的數(shù)據(jù)源。 

首先,實(shí)現(xiàn)一個(gè) Resource 類: 

代碼 

  1. //a generic class that describes a data with a status  

  2. public class Resource<T> {  

  3.     @NonNull public final Status status;  

  4.     @Nullable public final T data;  

  5.     @Nullable public final String message;  

  6.     private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {  

  7.         this.status = status;  

  8.         this.data = data;  

  9.         this.message = message;  

  10.     }  

  11.   

  12.     public static <T> Resource<T> success(@NonNull T data) {  

  13.         return new Resource<>(SUCCESS, data, null);  

  14.     }  

  15.   

  16.     public static <T> Resource<T> error(String msg, @Nullable T data) {  

  17.         return new Resource<>(ERROR, data, msg);  

  18.     }  

  19.   

  20.     public static <T> Resource<T> loading(@Nullable T data) {  

  21.         return new Resource<>(LOADING, data, null);  

  22.     }  

  23. }  


因?yàn)?,從網(wǎng)絡(luò)加載數(shù)據(jù)和從磁盤加載是很相似的,所以再新建一個(gè) NetworkBoundResource 類,方便多處復(fù)用。下面是 NetworkBoundResource 的決策樹: 

移動開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)


API 設(shè)計(jì): 

代碼 

  1. // ResultType: Type for the Resource data  

  2. // RequestType: Type for the API response  

  3. public abstract class NetworkBoundResource<ResultType, RequestType> {  

  4.     // Called to save the result of the API response into the database  

  5.     @WorkerThread  

  6.     protected abstract void saveCallResult(@NonNull RequestType item);  

  7.   

  8.     // Called with the data in the database to decide whether it should be  

  9.     // fetched from the network.  

  10.     @MainThread  

  11.     protected abstract boolean shouldFetch(@Nullable ResultType data);  

  12.   

  13.     // Called to get the cached data from the database  

  14.     @NonNull @MainThread  

  15.     protected abstract LiveData<ResultType> loadFromDb();  

  16.   

  17.     // Called to create the API call.  

  18.     @NonNull @MainThread  

  19.     protected abstract LiveData<ApiResponse<RequestType>> createCall();  

  20.   

  21.     // Called when the fetch fails. The child class may want to reset components  

  22.     // like rate limiter.  

  23.     @MainThread  

  24.     protected void onFetchFailed() {  

  25.     }  

  26.   

  27.     // returns a LiveData that represents the resource  

  28.     public final LiveData<Resource<ResultType>> getAsLiveData() {  

  29.         return result;  

  30.     }  

  31. }  


注意上面使用了 ApiResponse 作為網(wǎng)絡(luò)請求, ApiResponse 是對于 Retrofit2.Call 的簡單包裝,用于將其響應(yīng)轉(zhuǎn)換為 LiveData。 

下面是具體的實(shí)現(xiàn): 

代碼 

  1. public abstract class NetworkBoundResource<ResultType, RequestType> {  

  2.     private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();  

  3.   

  4.     @MainThread  

  5.     NetworkBoundResource() {  

  6.         result.setValue(Resource.loading(null));  

  7.         LiveData<ResultType> dbSource = loadFromDb();  

  8.         result.addSource(dbSource, data -> {  

  9.             result.removeSource(dbSource);  

  10.             if (shouldFetch(data)) {  

  11.                 fetchFromNetwork(dbSource);  

  12.             } else {  

  13.                 result.addSource(dbSource,  

  14.                         newData -> result.setValue(Resource.success(newData)));  

  15.             }  

  16.         });  

  17.     }  

  18.   

  19.     private void fetchFromNetwork(final LiveData<ResultType> dbSource) {  

  20.         LiveData<ApiResponse<RequestType>> apiResponse = createCall();  

  21.         // we re-attach dbSource as a new source,  

  22.         // it will dispatch its latest value quickly  

  23.         result.addSource(dbSource,  

  24.                 newData -> result.setValue(Resource.loading(newData)));  

  25.         result.addSource(apiResponse, response -> {  

  26.             result.removeSource(apiResponse);  

  27.             result.removeSource(dbSource);  

  28.             //noinspection ConstantConditions  

  29.             if (response.isSuccessful()) {  

  30.                 saveResultAndReInit(response);  

  31.             } else {  

  32.                 onFetchFailed();  

  33.                 result.addSource(dbSource,  

  34.                         newData -> result.setValue(  

  35.                                 Resource.error(response.errorMessage, newData)));  

  36.             }  

  37.         });  

  38.     }  

  39.   

  40.     @MainThread  

  41.     private void saveResultAndReInit(ApiResponse<RequestType> response) {  

  42.         new AsyncTask<Void, Void, Void>() {  

  43.   

  44.             @Override  

  45.             protected Void doInBackground(Void... voids) {  

  46.                 saveCallResult(response.body);  

  47.                 return null;  

  48.             }  

  49.   

  50.             @Override  

  51.             protected void onPostExecute(Void aVoid) {  

  52.                 // we specially request a new live data,  

  53.                 // otherwise we will get immediately last cached value,  

  54.                 // which may not be updated with latest results received from network.  

  55.                 result.addSource(loadFromDb(),  

  56.                         newData -> result.setValue(Resource.success(newData)));  

  57.             }  

  58.         }.execute();  

  59.     }  

  60. }  


現(xiàn)在,我們就能使用 NetworkBoundResource 來根據(jù)不同的情況獲取數(shù)據(jù)了: 

代碼 

  1. class UserRepository {  

  2.     Webservice webservice;  

  3.     UserDao userDao;  

  4.   

  5.     public LiveData<Resource<User>> loadUser(final String userId) {  

  6.         return new NetworkBoundResource<User,User>() {  

  7.             @Override  

  8.             protected void saveCallResult(@NonNull User item) {  

  9.                 userDao.insert(item);  

  10.             }  

  11.   

  12.             @Override  

  13.             protected boolean shouldFetch(@Nullable User data) {  

  14.                 return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));  

  15.             }  

  16.   

  17.             @NonNull @Override  

  18.             protected LiveData<User> loadFromDb() {  

  19.                 return userDao.load(userId);  

  20.             }  

  21.   

  22.             @NonNull @Override  

  23.             protected LiveData<ApiResponse<User>> createCall() {  

  24.                 return webservice.getUser(userId);  

  25.             }  

  26.         }.getAsLiveData();  

  27.     }  

  28. }  


到這里,我們的代碼就全部完成了。最后的架構(gòu)看起來就像這樣: 

移動開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)


最后的最后,給出一些指導(dǎo)原則 

下面的原則雖然不是強(qiáng)制性的,但根據(jù)我們的經(jīng)驗(yàn)遵循它們能使您的代碼更健壯、可測試和可維護(hù)的。 

  • 所有您在 manifest 中定義的組件 - activity, service, broadcast receiver… 都不是數(shù)據(jù)源。因?yàn)槊總€(gè)組件的生命周期都相當(dāng)短,并取決于當(dāng)前用戶與設(shè)備的交互和系統(tǒng)的運(yùn)行狀況。簡單來說,這些組件都不應(yīng)當(dāng)作為應(yīng)用的數(shù)據(jù)源。

  • 在您應(yīng)用的各個(gè)模塊之間建立明確的責(zé)任邊界。比如,不要將與數(shù)據(jù)緩存無關(guān)的代碼放在同一個(gè)類中。

  • 每個(gè)模塊盡可能少的暴露內(nèi)部實(shí)現(xiàn)。從過去的經(jīng)驗(yàn)來看,千萬不要為了一時(shí)的方便而直接將大量的內(nèi)部實(shí)現(xiàn)暴露出去。這會讓你在以后承擔(dān)很重的技術(shù)債務(wù)(很難更換新技術(shù))。

  • 在您定義模塊間交互時(shí),請考慮如何使每個(gè)模塊盡量隔離,通過設(shè)計(jì)良好的 API 來進(jìn)行交互。

  • 您應(yīng)用的核心應(yīng)該是能讓它脫穎而出的某些東西。不要浪費(fèi)時(shí)間重復(fù)造輪子或一次次編寫同樣的模板代碼。相反,應(yīng)當(dāng)集中精力在使您的應(yīng)用獨(dú)一無二,而將一些重復(fù)的工作交給這里介紹的 Android Architecture Components 或其他優(yōu)秀的庫。

  • 盡可能持久化數(shù)據(jù),以便您的應(yīng)用在脫機(jī)模式下依然可用。雖然您可能享受著快捷的網(wǎng)絡(luò),但您的用戶可能不會。