android各组件翻译,Android App框架指南(译文)
該系列文章是對Android推出的架構組件相關文章,按作者自己理解來翻譯的,同時標記有作者自己一些簡單筆記。如果讀者發現文中有翻譯不準確的地方,或者理解錯誤的地方,請不吝指教。
源自Android官方Guide to app architecturel principles 一文的翻譯與歸納
其他相關鏈接:
框架組件使用指導
Android Jetpack Components
[TOC]
前言
移動設備資源受限,在任何時候,系統都有可能回收某些應用進程,為新的進程騰出空間。因此你的應用組件隨時可能被系統或用戶中斷,或者在用戶重新打開應用時從某個中間點再次啟動。
這些事件不在你的控制之下,因此你不應該在應用程序的組件中存儲任何應用程序數據或狀態,并且這些應用組件之間不應相互依賴。
==Note:應用被回收從中間點恢復,需要將這些狀態數據集中管理。而不是直接保存到Activity或者Fragment中,這將導致你的界面數據很難恢復,回收之后再打開出現異常或者不得不從頭開始。==
如果不通過在應用組件中保存數據或狀態,那應該如何設計框架呢?
公共框架原則
關注點分離
最重要的原則就是分離關注點,簡單來說就是讓各個組件各自負責自己應該處理的邏輯,不應該將所有代碼都寫到Activity或者Fragment中。這些UI相關的類應該只處理UI或者交互相關的邏輯,保持這些類的精簡可以更好地避免生命周期相關的問題。
==Note:簡化UI類邏輯可以使數據、異步等處理邏輯在各個生命周期體現得更清晰==
請記住,你并沒有實現Activity或Fragment,它們只是你和Android操作系統溝通使用的類。系統將用戶交互通過這些類來傳達給開發者,也可以在低內存等條件下隨時銷毀他們。所以為了更好地管理你的程序,提升用戶體驗,需要減少對它們的依賴。
==Note:我們應該把這些類當做是系統提供的接口,而不是要去實現它。我們應該把自己的業務邏輯分離出來,利用這些接口與用戶交互。==
從模型中驅動UI
另一個重要的原則是通過模型來驅動你的UI,最好是持久模型。模型是負責處理應用數據的組件,他們獨立于應用中的View對象和應用組件(四大組件和其它組件),所以他們不應該受應用程序生命周期和相關問題的影響。
==Note:模型層本身是數據處理中心,持久化的模型層是不受應用組件影響的。應該將Model層分離出來,通過ViewModel驅動UI組件與用戶產生交互。==
數據能持久化是最理想的,原因如下:
如果Android系統銷毀你的應用以釋放資源,用戶不會丟失數據。
如果網絡連接不穩定或無法使用,你的應用仍可繼續使用。
將模型類按數據管理職責明確劃分,可以讓代碼測試、閱讀更加輕松。
推薦的應用框架
在本節中,將演示如何使用Architecture Components來構建端到端的應用程序。
設計一個架構適合每種場景的應用程序是不可能的,但是我們推薦的架構適用于大多數情況和工作流程。如果你已經有一種很好地符合公用框架原則的結構,那么你不需要改變他。
想象一下,我們正在構建一個顯示用戶配置的界面,通過服務器私有API來獲取配置數據。
概覽
首先,請思考下圖,下圖顯示了應用程序各個模塊相互應該如何交互。
概覽圖
注意,每一級組件僅依賴于下一級組件。舉個例子,Activity與Fragment僅依賴ViewModel層,Repository 依賴于持久模型和遠端后臺數據源。
==Note:在該設計結構中,分層和層次間的依賴關系是關鍵,每個層級不應該出現越級、交叉之類的依賴關系。同時需要注意ViewModel并不是Model,他是Model層與View層的中間層,類似在MVP中Presenter的角色,與Repository的通信的數據處理業務邏輯應該放到這里。==
這種設計能帶來一致和愉悅的用戶體驗。無論用戶在關閉應用幾分鐘后還是幾天后再回到應用程序,都可以立即看到應用程序在本地持久化的數據。如果數據已經過期,Repository會在后臺更新數據,并在更新完成后呈現給用戶。
構建用戶接口
UI 由 UserProfileFragment 和他的布局文件 user_profile_layout.xml 組成。
為了驅動UI,我們的數據模型需要包含以下元素:
User ID: 用戶的唯一標識,最好通過fragment的setArguments方法來傳遞該參數。如果系統銷毀我們的進程,這些信息會被保留,因此ID將在下次重新啟動時再次使用。
User Object:包含用戶詳細信息的數據類。
我們使用基于 ViewModel 框架組件的 UserProfileViewModel 類來保存這些信息。
一個ViewModel對象只給特定的activity或fragment提供數據,并包含model通信和數據處理邏輯。
舉個例子,ViewModel可以通知其他組件加載數據,也可以根據用戶請求來修改數據。
ViewModel不需要了解UI組件,因此它不受配置更改的影響,例如在旋轉屏幕后重建activity。
==Note: ViewModel不只是UI組件的一個對象,即使UI組件在某些情況下銷毀重建,ViewModel也不一定會丟失,可以被重建后的UI組件繼續使用。同時由于持久化的特性,即使ViewModel回收重建,也可以從Model中重新加載數據。==
目前為止我們定義了以下文件:
user_profile_layout.xml: 界面的布局文件
UserProfileFragment: 控制UI顯示數據的組件
UserProfileViewModel:為UserProfileFragment準備需要顯示的數據以及對用戶交互做出反應的類
以下代碼片段顯示了這些類的初步內容。(省略布局文件代碼)
UserProfileViewModel
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
UserProfileFragment
public class UserProfileFragment extends Fragment {
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);
}
}
現在我們有了這些代碼模塊,我們應該如何關聯他們?畢竟,當UserProfileViewModel設置user對象時,我們需要一種方式通知UI更新。而這正是LiveData組件的作用。
LiveData是一個可觀察數據的持有者,應用程序中的其他組件可以使用LiveData觀察數據改變,而不需要在他們之間創建明確且嚴格的依賴關系。LiveData組件還遵循activity、fragment和service這些組件的生命周期,且已經包含清理邏輯,以防止內存泄露和過多的內存占用。
將LiveData引入到我們的應用程序中,我們將UserProfileViewModel中的user字段類型改為LiveData。現在,數據更新時可以通知到UserProfileFragment了。此外,由于LiveData可以識別組件生命周期,當不再需要它其中存放的數據后會自動清除組件的引用。
==Note:這里查看LiveData源碼,LiveData可以識別Activity、Fragment、Service等組件的生命周期,當生命周期為Destroy時,會自動將該Observer移除。==
UserProfileViewModel
public class UserProfileViewModel extends ViewModel {
...
private LiveData user;
public LiveData getUser() {
return user;
}
}
現在我們修改UserProfileFragment以觀察數據改變并更新UI:
UserProfileFragment
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, new Observer() {
@Override
public void onChanged(User user) {
// 更新UI
}
});
}
每當用戶配置數據更新,onChanged()都會被調用,同時會更新UI。
如果你對observable相關庫比較熟悉的話,你可能會注意到我們沒有在fragment的onStop()里停止數據觀察。因為LiveData可以根據生命周期自己處理,所以這一步是沒有必要的。也就是說只會在fragment處于活躍狀態時(已經onStart()但沒有onStop()),才會回調onChanged()方法;LiveData也會在fragment的onDestroy()方法調用后自動移除觀察者的引用。
我們也不需要添加任何處理configuration改變的邏輯,比如用戶旋轉屏幕。UserProfileViewModel會在configuration改變后自動回復,并立即使用當前數據調用回調。鑒于ViewModel的存活時間比他們更新的View更長,在實現ViewModel時不應該包含對View對象的直接引用。需要獲取更多關于ViewModel生命周期的信息,可以參考 The lifecycle of a ViewModel。
==Note1:關于上面的描述,查看源碼后了解到:FragmentActivity和Fragment在新版本support包里都增加了一個ViewModelStore對象,該對象負責保存這個View創建的所有ViewModel,并且在銷毀的使用統一清理。值得一提的是,當activity的onDestroy()方法調用,但是mRetaining為true時,ViewModelStore不會清理,所以重建后依然可以正常使用。==
==Note2:文中提到ViewModel的存活時間比他們更新的View更長,是因為如果activity重建,沒有特殊配置的情況下會重新生成一個新的Activity對象,而新的Activity對象使用的依然是重建前的ViewModelStore。如果ViewModel引用了之前的activity,就會產生內存泄露,fragment是一樣的原理。另外fragment的ViewModelStore是Activity利用FragmentManager來恢復的。==
獲取數據
現在我們已經使用LiveData將UserProfileViewModel連接到UserProfileFragment,下一步我們思考應該怎樣從服務端獲取數據。
我們假設服務端提供了一個REST API。使用 Retrofit 與后端通信,當然你也可以用使用其它的庫來實現。
這里定義WebService與后端通信
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直接調用WebService獲取數據,并將數據分配給LiveData對象。這個邏輯是可行的,但是如果使用這個邏輯,隨著功能增多程序會越來越難維護。這會讓UserProfileViewModel做太多工作,違反了關注點分離這一原則。另外,ViewModel的生命周期與Activity或Fragment相關聯,也就是說如果UI組件生命周期結束,通過Webservice獲取的數據將會丟失。這會讓產生不好的用戶體驗。
所以,我們的ViewModel將獲取數據的過程交給Repository模塊來完成
Repository 模塊負責處理數據相關操作。他們提供一些簡潔的API以便其他模塊能輕松獲取這些數據。他們知道從哪里獲取數據以及在什么時候更新數據。你可以認為Repository是不同數據源(持久模型、Web服務和緩存)之前的調解器.
==Note:進一步定義ViewModel的職責,ViewModel本身也不應該關心數據來源,這些邏輯應該交給Repository完成。ViewModel只是簡單加工數據并通知View更新,以及處理View層產生的用戶交互行為。Repository負責數據獲取和本地持久化接口調用,保證之后ViewModel獲取數據可以很快獲取到緩存數據。這樣的結構也可以解決我們應用里每次進入都需要重新獲取數據才能正常使用的硬傷。==
我們的UserRepository類(如以下代碼所示),使用WebService實例來獲取用戶的數據。
UserRepository
public class UserRepository {
private Webservice webservice;
// ...
public LiveData getUser(int userId) {
// This isn't an optimal implementation. We'll fix it later.
final MutableLiveData data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
data.setValue(response.body());
}
// Error case is left out for brevity.
});
return data;
}
}
上面代碼看起來repository模塊不是必要的,但他有一個重要的目的:作為app其他模塊的數據源?,F在,我們的UserProfileViewModel不知道怎么獲取數據,我們可以為它提供幾個不同的數據獲取方式。
管理組件之間的依賴
在UserRepository獲取用戶數據前,需要一個WebService的實例。當然我們可以簡單的創建實例,但如果這樣做,還需要知道WebService的依賴關系(創建實例需要傳參或者初始化)。另外,UserRepository可能并不是唯一需要使用WebService的類。這樣的需求會造成重復的代碼,因為每個使用WebService的類都需要知道它的依賴關系。如果每個類都創建一個WebService,我們的應用可能變得非常臃腫。
你可以使用以下設計模式來解決此問題:
Dependency injection(依賴注入):依賴注入允許類在不構造它們的情況下定義它們的依賴關系。在運行時,另外的類負責提供這些依賴項。我們推薦Dragger2來實現Android應用的依賴注入。Dragger2通過遍歷依賴樹自動構造對象,并為依賴關系提供編譯時保證。
Service locator(服務定位器):服務定位器模式提供一個注冊表,其中的類可以獲取它們的依賴而不用構造它們。
實現服務注冊表比使用依賴注入更簡單,如果你不熟悉依賴注入,可以考慮使用服務定位器模式。
這些設計模式提供了清晰的模式管理依賴項,你無須復制代碼或增加復雜性就能擴展代碼功能。此外,這些模式允許你能在獲取測試和生產數據的之間快速切換。
我們的示例使用 Dragger 2 來管理WebService對象的依賴。
連接 ViewModel 與 Repository
現在,我們增加UserProfileViewModel使用UserRepository對象的代碼:
UserProfileViewModel
public class UserProfileViewModel extends ViewModel {
private LiveData user;
private UserRepository userRepo;
// Instructs Dagger 2 to provide the UserRepository parameter.
@Inject
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(int userId) {
if (this.user != null) {
// ViewModel is created on a per-Fragment basis, so the userId
// doesn't change.
return;
}
user = userRepo.getUser(userId);
}
public LiveData getUser() {
return this.user;
}
}
緩存數據
UserRepository將對WebService的調用抽象出來,但是他不是很靈活(通用),因為他只依靠一個數據源。
UserRespository的關鍵問題在于他總是從后端獲取數據,并沒有將數據存儲下來。因此如果用戶離開UserProfileFragment,再返回來時必須重新獲取數據,即使數據沒有發生改變。
下面是該設計并不理想的原因:
它浪費了多余的網絡帶寬
他強制要求用戶等待查詢完成
為了解決這些問題,我們在UserRepository中添加一個新的數據源,用于在內存中緩存User對象:
UserRepository
// Informs Dagger that this class should be constructed only once.
@Singleton
public class UserRepository {
private Webservice webservice;
// Simple in-memory cache. Details omitted for brevity.
private UserCache userCache;
public LiveData getUser(int userId) {
LiveData cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData data = new MutableLiveData<>();
userCache.put(userId, data);
// This implementation is still suboptimal but better than before.
// A complete implementation also handles error cases.
webservice.getUser(userId).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
data.setValue(response.body());
}
});
return data;
}
}
持久化數據
根據我們當前的實現,由于Repository能夠從內存中查找數據,如果用戶旋轉屏幕,或者離開后立即回到App,界面都能立即顯示。
然而,如果用戶離開幾個小時后,Android系統殺掉應用進程,用戶再打開應用,會發生什么呢?這種情形下根據我們當前的實現,需要重新連接網絡獲取數據。這種重新獲取的過程不僅僅是糟糕的用戶體驗,也會浪費寶貴的移動數據流量。
==Note:根據數據的實時性和體量,將數據量大、實時性小的數據做持久化緩存。而通過少了數據標識來決定是否對持久化數據更新,已達到數據流量的最大利用,也能用戶重啟應用帶來良好體驗。==
你可以通過緩存Web請求來解決這個問題,但是這會產生另一個問題:如果相同的用戶數據也可以來自其他類型的請求,比如請求好友列表,兩份數據獲取時間不一致,導致兩份數據相同部分可能不一致。舉個例子,如果用戶在不同時間發出好友列表請求和單個用戶請求,我們的應用可能會顯示同一用戶數據的兩個版本。我們的應用需要弄清楚如何合并這些不一致的數據。
==Note:這也是一個常見問題,比如列表數據獲取后,點擊列表內進入詳情頁面獲取詳情數據,但是詳情數據相對列表數據已經發生改變,就會造成里外不一致的情況。==
處理這種情況最好的方式是使用持久模型,這就是Room來拯救的地方
Room是一個對象映射庫,提供本地數據持久化,并且只有很小的代碼體積。在編譯時,它根據你創建的每個數據模型驗證每個查詢,因此錯誤的SQL查詢會導致編譯錯誤,而不是運行時失敗。Room封裝了使用原始SQL查詢和一些底層實現細節。他也允許你觀察數據庫數據的改變,包括集合查詢和多表查詢,通過LiveData對象來通知這些改變。他甚至明確定義了執行約束來解決常見的線程問題,例如在主線程訪問storage。
要使用Room,我們需要定義本地模型。首先,我們添加@Entity注解到User模型類,同時為id字段添加@PrimaryKey。這些注解讓User像數據庫中的一張表,而id則是表的主鍵。
User
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// Getters and setters for fields.
}
然后,我們實現RoomDatabase類來創建數據庫:
UserDatabase
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
}
注意,UserDatabase是抽象的,Room會自動提供它的實例,更多細節請參考Room文檔
我們現在需要一種方式將用戶數據插入數據庫。為此我們創建一個數據訪問對象(DAO)。
UserDao
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData load(int userId);
}
注意load方法返回了一個LiveData對象。Room知道數據庫何時被修改,并在數據改變時自動通知所有活躍狀態的觀察者。因為Room使用了LiveData,因此該操作非常有效,它僅在至少有一個活動觀察者時才更新數據。
==Note:LiveData在分發數據改變事件時,會判斷觀察者是否處于活躍狀態,如果不是則不會處理。==
在定義UserDao類之后,我們從數據庫類中引用DAO
UserDatebase
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
現在,我們可以修改UserRepository來引入本地數據源。
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 getUser(String userId) {
refreshUser(userId);
// Returns a LiveData object directly from the database.
return userDao.load(userId);
}
private void refreshUser(final String userId) {
// Runs in a background thread.
executor.execute(() -> {
// Check if user data was fetched recently.
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// Refreshes the data.
Response response = webservice.getUser(userId).execute();
// Check for errors here.
// Updates the database. The LiveData object automatically
// refreshes, so we don't need to do anything else here.
userDao.save(response.body());
}
});
}
}
請注意,即使我們在UserRepository中改變的數據來源,也不需要改變UserProfileViewModel或者UserProfileFragment。這個小范圍的更新展示了我們應用框架提供的靈活性。也更有利于測試,因為我們可以提供一個模擬的UserRepository來測試UserProfileViewModel和其它組件。
使用這種結構,如果用戶等待幾天后再次打開應用,在repository更新數據之前,他們可以先看到本地過期的數據。當然,你可以能并不希望展示這些過期信息,你可以先展示一些占位數據并提示你的應用正在加載最新信息。
統一的數據源
不同的REST API接口通常會返還相同的數據。比如,如果我們的后臺有另一個接口返回好友列表,那么相同一個用戶對象可能來自兩個接口,甚至可能使用不同級別的粒度(用戶接口獲取的信息比列表中的用戶對象更詳細)。如果UserRepository按原樣從WebService請求數據,而不檢查一致性,我們的UI可能會展示不一樣的數據,因為最近調用的接口決定了repository的數據版本和格式。
由于這個原因,我們的UserRepository實現將Web服務返回的數據保存到數據庫中,而改變數據庫會觸發LiveData的回調。通過該模型,數據庫提供統一的數據源,應用的其他部分使用UserRepository訪問該數據源。無論是否使用disk cache,我們都建議你的repository將數據源統一成唯一的實際源頭。
顯示加載中
在一些用例中,比如下拉刷新,向用戶展示正在進行網絡操作是非常重要的。將UI操作與實際數據分離是一種很好的做法,因為數據可能會因為各種原因而更新。舉個例子,假設我們獲取一個好友列表,同一個用戶信息可能會以編程的方式再次獲取,從而觸發LiveData的刷新。從UI的角度來看,這次請求只是另一個數據獲取點,類似于獲取User對象本身。
我們可以使用以下策略來保證在UI中顯示一致的數據更新指示,而不用管更新數據的請求來自何處:
修改getUser()方法返回LiveData對象的類型,這個對象可能包含一個網絡操作狀態。
可以參考android-architecture-components中的NetworkBoundResource的實現。
在UserRepository中提供另一個可以返回刷新狀態的方法。如果數據只會通過用戶操作更新,那么這種方式會更好。
測試每個組件
這一段沒翻譯,大家可以自己看看原文
最佳實踐
編程是一個創造性領域,構建Android應用也不例外。無論是在各個界面傳遞數據,或是獲取后端數據并在本地持久化,還是一些其它重要常見,都有許多方法可以解決問題。
雖然以下建議都不是強制性的,但根據我們的經驗來看,遵循他們可以使您的代碼庫在長期運行中更加可靠、健壯、可維護。
避免將你的應用入口點作為數據源(例如activity、service和boardcast)
在應用各個模塊之間明確定義責任范圍
每個模塊盡可能少地暴露,降低使用的學習成本
考慮如果讓每個模塊可以單獨測試
專注于你的應用的核心,從其它應用中脫穎而出
盡可能保證數據的及時性和相關性
創建單一數據源以保證數據的統一性和有效性
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的android各组件翻译,Android App框架指南(译文)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql按中文拼音字母排序_解析MyS
- 下一篇: redhat5.4 安装mysql_Li