Google 官方推出應用開發架構

Google 官方推出應用開發架構

最近,官方推出了一份關於應用架構的最佳實踐指南。這裡就給大家簡要介紹一下:

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

這也就引出了第一條準則:「不要在應用程序組件中保存任何應用數據或狀態,並且組件間也不應該相互依賴」。

最常見的錯誤就是在 Activity 或 Fragment 中寫了與 UI 和交互無關的代碼。儘可能減少對它們的依賴,這能避免大量生命週期導致的問題,以提供更好的用戶體驗。

第二條準則:「通過 model 驅動應用 UI,並儘可能的持久化」。

這樣做主要有兩個原因:

  1. 如果系統回收了你的應用資源或其他什麼意外情況,不會導致用戶丟失數據。

  2. Model 就應該是負責處理應用程序數據的組件。獨立於視圖和應用程序組件,保持了視圖代碼的簡單,也讓你的應用邏輯更容易管理。並且,將應用數據置於 model 類中,也更有利於測試。

官方推薦的 App 架構

在這裡,官方演示了通過使用最新推出的 Architecture Components 來構建一個應用。

想象一下,您正在打算開發一個顯示用戶個人信息的界面,用戶數據通過 REST API 從後端獲取。

首先,我們需要創建三個文件:

  • user_profile.xml:定義界面。

  • UserProfileViewModel.java:數據類。

  • UserProfileFragment.java:顯示 ViewModel 中的數據並對用戶的交互做出反應。

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

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;  public LiveData getUser() {  return user;  }}

再在 UserProfileFragment 中對其進行觀察並更新我們的 UI:

@Overridepublic 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 getUser(@Path("user") String userId);}

不要通過 ViewModel 直接來獲取數據,這裡我們將工作轉交給一個新的 Repository 模塊。

Repository 模塊負責數據處理,為應用的其他部分提供乾淨可靠的 API。你可以將其考慮為不同數據源(Web,緩存或數據庫)與應用之間的中間層。
public class UserRepository {  private Webservice webservice;  // ...  public LiveData getUser(int userId) {  // This is not an optimal implementation, we'll fix it below  final MutableLiveData data = new MutableLiveData<>();   webservice.getUser(userId).enqueue(new Callback() {  @Override  public void onResponse(Call call, Response 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;  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 getUser() {  return this.user;  } }

緩存數據

在實際項目中,Repository 往往不會只有一個數據源。因此,我們這裡在其中再加入緩存:

@Singleton // informs Dagger that this class should be constructed oncepublic class UserRepository {  private Webservice webservice;  // simple in memory cache, details omitted for brevity  private UserCache userCache;  public LiveData getUser(String userId) {  LiveData cached = userCache.get(userId);  if (cached != null) {  return cached;  }  final MutableLiveData 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() {   @Override  public void onResponse(Call call, Response response) {  data.setValue(response.body());  }  });  return data;  } } 

持久化數據

現在當用戶旋轉屏幕或暫時離開應用再回來時,數據是直接可見的,因為是直接從緩存中獲取的數據。但要是用戶長時間關閉應用,並且 Android 還徹底殺死了進程呢?

我們目前的實現中,會再次從網絡中獲取數據。這可不是一個好的用戶體驗。這時就需要數據持久化了。繼續引入一個新組件 Room。

Room 能幫助我們方便的實現本地數據持久化,抽象出了很多常用的數據庫操作,並且在編譯時會驗證每個查詢,從而損壞的 SQL 查詢只會導致編譯時錯誤,而不是運行時崩潰。還能和上面介紹的 LiveData 完美合作,並幫開發者處理了很多線程問題。

現在,讓我們來看看怎麼使用 Room 吧。: )

@Entityclass 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 會自動添加實現的。

現在我們需要一種方法來將用戶數據插入到數據庫:

@Daopublic interface UserDao { @Insert (onConflict = REPLACE) void save(User user);   @Query("SELECT * FROM user WHERE id = :userId")   LiveData 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:

@Singletonpublic 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 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 statuspublic class Resource {  @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  Resource success(@NonNull T data) {  return new Resource<>(SUCCESS, data, null); } public static  Resource error(String msg, @Nullable T data) {  return new Resource<>(ERROR, data, msg); }public static  Resource loading(@Nullable T data) {  return new Resource<>(LOADING, data, null); }}

因為,從網絡加載數據和從磁盤加載是很相似的,所以再新建一個 NetworkBoundResource 類,方便多處複用。下面是 NetworkBoundResource 的決策樹:

Google 官方推出應用開發架構

API 設計:

// ResultType: Type for the Resource data// RequestType: Type for the API responsepublic abstract class NetworkBoundResource {  // 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 loadFromDb();  // Called to create the API call.   @NonNull @MainThread  protected abstract LiveData> 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> getAsLiveData() {  return result;  }}

注意上面使用了 ApiResponse 作為網絡請求, ApiResponse 是對於 Retrofit2.Call 的簡單包裝,用於將其響應轉換為 LiveData。

下面是具體的實現:

public abstract class NetworkBoundResource {  private final MediatorLiveData> result = new MediatorLiveData<>();  @MainThread  NetworkBoundResource() {  result.setValue(Resource.loading(null));  LiveData 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 dbSource) {   LiveData> 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 response) {  new AsyncTask() {   @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> loadUser(final String userId) {  return new NetworkBoundResource() {  @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 loadFromDb() {  return userDao.load(userId);  }  @NonNull @Override  protected LiveData> createCall() {  return webservice.getUser(userId);  }  }.getAsLiveData();  }} 

到這裡,我們的代碼就全部完成了。最後的架構看起來就像這樣:

Google 官方推出應用開發架構

最後的最後,給出一些指導原則

下面的原則雖然不是強制性的,但根據我們的經驗遵循它們能使您的代碼更健壯、可測試和可維護的。

  • 所有您在 manifest 中定義的組件 - activity, service, broadcast receiver... 都不是數據源。因為每個組件的生命週期都相當短,並取決於當前用戶與設備的交互和系統的運行狀況。簡單來說,這些組件都不應當作為應用的數據源。

  • 在您應用的各個模塊之間建立明確的責任邊界。比如,不要將與數據緩存無關的代碼放在同一個類中。

  • 每個模塊儘可能少的暴露內部實現。從過去的經驗來看,千萬不要為了一時的方便而直接將大量的內部實現暴露出去。這會讓你在以後承擔很重的技術債務(很難更換新技術)。

  • 在您定義模塊間交互時,請考慮如何使每個模塊儘量隔離,通過設計良好的 API 來進行交互。

  • 您應用的核心應該是能讓它脫穎而出的某些東西。不要浪費時間重複造輪子或一次次編寫同樣的模板代碼。相反,應當集中精力在使您的應用獨一無二,而將一些重複的工作交給這裡介紹的 Android Architecture Components 或其他優秀的庫。

  • 儘可能持久化數據,以便您的應用在脫機模式下依然可用。雖然您可能享受著快捷的網絡,但您的用戶可能不會。


分享到:


相關文章: