【譯】LiveData三連

點擊上方藍(lán)字關(guān)注我,知識會給你力量

這個系列我做了協(xié)程和Flow開發(fā)者的一系列文章的翻譯,旨在了解當(dāng)前協(xié)程、Flow、LiveData這樣設(shè)計的原因,從設(shè)計者的角度,發(fā)現(xiàn)他們的問題,以及如何解決這些問題,pls enjoy it。
When and why to use Android LiveData
差不多一年前(2017年5月的第一個alpha版本),谷歌發(fā)布了 "安卓架構(gòu)組件",這是一個庫的集合,旨在幫助安卓開發(fā)人員設(shè)計更強大、可測試和可維護(hù)的應(yīng)用程序。最引人注目的是LiveData類和相關(guān)的生命周期感知類、Room持久性庫和新的分頁庫。在這篇文章中,我將探討LiveData類,它期望希望解決的問題以及何時去使用這個庫。
?老實說,LiveData是一個可觀察的數(shù)據(jù)持有者。它讓你的應(yīng)用程序中的組件,通常是UI,能夠觀察LiveData對象的變化。
?
關(guān)于這個LiveData的新概念是,它具有生命周期意識,這意味著它尊重應(yīng)用程序組件(Activity、Fragment)的生命周期狀態(tài),并確保LiveData只在組件(觀察者)處于活躍的生命周期狀態(tài)時更新它。這種行為可以防止內(nèi)存泄漏,確保應(yīng)用程序不會做更多無效的工作。
為了更好地理解何時使用這個新的可觀察的數(shù)據(jù)持有者以及使用它的優(yōu)勢,在這篇文章的其余部分,我將回顧一些替代方案,以面對根據(jù)數(shù)據(jù)變化更新UI這一基本任務(wù)。
在UI組件中管理數(shù)據(jù) 使用一個監(jiān)聽器接口 使用事件總線 使用LiveData 總結(jié)
但首先,讓我們介紹一下我們的示例方案。
Scenario
為了用代碼片段進(jìn)行演示,我們想象一下,構(gòu)建一個社交網(wǎng)絡(luò)應(yīng)用中的界面UI,它顯示了一個用戶的簡介以及該用戶的關(guān)注者數(shù)量。在簡介圖片和當(dāng)前關(guān)注者數(shù)量的下方,有一個切換按鈕,讓當(dāng)前登錄的用戶可以關(guān)注/取消關(guān)注該用戶。我們希望這個按鈕能夠影響帶有關(guān)注者數(shù)量的標(biāo)簽,并相應(yīng)地改變按鈕上的文字。(代碼將使用Java語言)。

#1 — Managing data in the UI component
一個天真的想法是將UI組件(Activity、Fragment)變成“上帝對象”。你首先從框架提供給你的唯一的UI組件開始,比如說一個Activity,你在那里寫你的應(yīng)用程序的UI代碼。后來,當(dāng)你需要處理數(shù)據(jù)并在此基礎(chǔ)上改變UI時,你會發(fā)現(xiàn)繼續(xù)在活動中寫代碼,這樣會更容易,因為它已經(jīng)包含了所有需要更新的字段和UI元素。讓我們來看看代碼會是什么樣子。
public class UserProfileActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
isFollowing = webService.getIsFollowing();
numberOfFollowers = webService.getNumberOfFollowers();
toggleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toggleFollow();
}
});
}
private void toggleFollow() {
if (isFollowing)
unFollow();
else
follow();
}
private void unFollow() {
isFollowing = false;
numberOfFollowers -= 1;
followersText.setText(numberOfFollowers + " Followers");
setNotFollowingButton();
}
private void follow() {
isFollowing = true;
numberOfFollowers += 1;
followersText.setText(numberOfFollowers + " Followers");
setFollowingButton();
}
private void setFollowingButton() {
toggleButton.setText("Following");
toggleButton.setBackground(getLightGreenColor());
}
private void setNotFollowingButton() {
toggleButton.setText("Follow");
toggleButton.setBackground(getGreenColor());
}
}
這種方式是一種反面模式,它違反了編寫良好設(shè)計的代碼的一些關(guān)鍵原則,當(dāng)涉及到數(shù)據(jù)和持久性時,這種方法還有一個重大缺陷。
那就是數(shù)據(jù)和狀態(tài)的丟失——像Activity或Fragment這樣的應(yīng)用程序組件不是由我們管理,而是由系統(tǒng)管理。因為它們的生命周期不在我們的控制之下,它們可以在任何時候根據(jù)用戶的互動或其他因素(如低內(nèi)存)被銷毀。如果我們在一個UI組件中創(chuàng)建和處理我們的數(shù)據(jù),一旦該組件被銷毀,我們所有的數(shù)據(jù)都會被銷毀。在這個例子中,例如每次用戶旋轉(zhuǎn)設(shè)備時,該Activity就會被銷毀并重新創(chuàng)建,導(dǎo)致所有的數(shù)據(jù)被重置,網(wǎng)絡(luò)調(diào)用再次被執(zhí)行,浪費了用戶的流量,迫使用戶等待新的查詢完成等操作。
#2 — Using a listener interface
解決這個基于數(shù)據(jù)變化更新UI的任務(wù)的另一種方法是,使用監(jiān)聽器接口,它給UI監(jiān)聽器施加了一個特定的功能。
// IProfileRepository.java
public interface IProfileRepository {
boolean isFollowing();
void toggleFollowing();
int getNumberOfFollowers();
}
// ProfileController.java
public class ProfileController {
public void showProfile(IProfileListener listener) {
boolean isFollowing = profileRepo.getIsFollowing();
if (isFollowing)
listener.setFollowingButton(getLightGreenColor(),"Following");
else
listener.setFollowingButton(getGreenColor(), "Follow");
listener.setFollowersText(profileRepo.getNumberOfFollowers());
}
public void toggleFollowing(IProfileListener listener) {
profileRepo.toggleFollowing();
showProfile(listener);
}
public interface IProfileListener {
void setFollowingButton(int color, String text);
void setFollowersText(int numberOfFollowers);
}
}
// UserProfileActivity.java
public class UserProfileActivity extends AppCompatActivity, implements IProfileListener {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
toggleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
profileController.toggleFollowing(UserProfileActivity.this);
}
});
profileController.showProfile(this);
}
@Override
public void setFollowingButton(int color, String text) {
toggleButton.setBackground(color);
toggleButton.setText(text);
}
@Override
public void setFollowersText(int numberOfFollowers) {
followersText.setText(numberOfFollowers + " Followers");
}
}
為了簡潔起見,我省略了一些接口的聲明和一些實現(xiàn)。首先,我們來看看這個解決方案。Activity本身并沒有意識到用戶關(guān)注者的數(shù)據(jù)信息變化。它唯一關(guān)心的是顯示一個帶有文本的用戶界面,用戶可以在那里點擊一個按鈕。請注意,現(xiàn)在的Activity不包含任何一行if條件代碼。Activity根據(jù)ProfileController來獲取數(shù)據(jù)。ProfileController反過來使用ProfileRepository來獲取數(shù)據(jù),無論是從網(wǎng)絡(luò)(使用先前在Activity中使用的WebService)還是從其他地方(如內(nèi)存緩存或持久化)。一旦ProfileController得到數(shù)據(jù)并準(zhǔn)備好更新用戶界面,它就會回調(diào)傳入的監(jiān)聽器(實際上是Activity)并調(diào)用它的一個方法。實際上,ProfileController是邁向MVP設(shè)計(Model-View-Presenter)的第一步。我們可以將Controller設(shè)置為使用更多的迷你Controller,每個Controller都會自己改變相應(yīng)的UI元素,從而將改變UI的功能完全從活動中提取出來。
這種方案避免了UI組件被破壞后的數(shù)據(jù)丟失問題,對于正確分離代碼中的關(guān)注點很有用。此外,它使UI組件的代碼保持干凈和盡可能的精簡,從而使我們的代碼更容易維護(hù),并且一般來說,我們可以避免許多與生命周期有關(guān)的問題。但這種有效方法的主要缺點是,它有些容易出錯,如果你不夠小心,你會發(fā)現(xiàn)自己造成了一個異?;虮罎ⅰ_@個簡單的例子有點難以證明,但對于更復(fù)雜和真實的場景,錯誤是一定會發(fā)生的。例如,如果Activity經(jīng)歷了配置的改變,你的監(jiān)聽器引用可能是空的。另一個例子是,當(dāng)你的監(jiān)聽器的生命周期是不活躍的,比如在后堆棧中的Activity,但你依然試圖將事件傳遞給它并調(diào)用它的功能。一般來說,這種方法要求你了解監(jiān)聽器(UI組件)的生命周期,并在你的代碼中考慮到它。對于像Kotlin這樣函數(shù)是一等公民的語言來說也是如此。盡管你可以將一個函數(shù)作為參數(shù)而不是UI組件本身傳遞,但在這里你也應(yīng)該知道UI組件的生命周期,因為該函數(shù)通常會操作該組件的UI元素。
#3 — Using an event bus
另一種方法是,當(dāng)我們必須根據(jù)數(shù)據(jù)變化更新用戶界面時,使用基于事件的機(jī)制(發(fā)布者/訂閱者)(使用greenrobot EventBus演示)。
// IProfileRepository.java
public interface IProfileRepository {
boolean isFollowing();
void toggleFollowing();
int getNumberOfFollowers();
}
// ProfileController.java
public class ProfileController {
public void showProfile() {
int numberOfFollowers = profileRepo.getNumberOfFollowers();
boolean isFollowing = profileRepo.getIsFollowing();
if (isFollowing)
EventBus.getDefault().post(
new UpdateFollowStatusEvent(getLightGreenColor(),"Following", numberOfFollowers));
else
EventBus.getDefault().post(
new UpdateFollowStatusEvent(getGreenColor(), "Follow", numberOfFollowers));
}
public void toggleFollowing() {
profileRepo.toggleFollowing();
showProfile();
}
}
// UpdateFollowStatusEvent.java
public class UpdateFollowStatusEvent {
public int buttonColor;
public String buttonText;
public int numberOfFollowers;
public UpdateFollowStatusEvent(int buttonColor, String buttonText, int numberOfFollowers) {
this.buttonColor = buttonColor;
this.buttonText = buttonText;
this.numberOfFollowers = numberOfFollowers;
}
}
// UserProfileActivity.java
public class UserProfileActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
toggleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
profileController.toggleFollowing();
}
});
profileController.showProfile();
}
@Subscribe
public void onStatusUpdateEvent(FollowStatusUpdateEvent event) {
toggleButton.setBackground(event.buttonColor);
toggleButton.setText(event.buttonText);
followersText.setText(event.numberOfFollowers + " Followers");
}
@Override
protected void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
protected void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
}
正如你所看到的,這個解決方案與之前的解決方案非常相似。所不同的是,我們不是調(diào)用監(jiān)聽器的方法,而是觸發(fā)事件。這些事件被訂閱者攔截,在我們的例子中就是Activity,然后用戶界面就會相應(yīng)地改變。
在社區(qū)內(nèi)有一個激烈的討論,即事件總線是否是一個好的解決方案,或者說監(jiān)聽器回調(diào)是否是真正的解決方案。無論如何,這種技術(shù),作為監(jiān)聽器接口,也避免了數(shù)據(jù)丟失,并保持代碼中的職責(zé)分離。此外,實現(xiàn)基于事件機(jī)制的庫通常支持高級功能,如交付線程和訂閱者的優(yōu)先級(人們也可以使用Android LocalBroadcast,而不需要第三方庫)。相對于監(jiān)聽器接口的方法,基于事件的方法不需要你考慮生命周期的問題,因為大多數(shù)庫都會為你考慮這一點。然而,你需要注意注冊(和取消注冊)訂閱者,以便他們能夠接收事件,如果不能正確地這樣做,可能會導(dǎo)致不被注意的內(nèi)存泄漏。此外,盡管事件總線一開始看起來很方便實現(xiàn),但它很快就會變成一個遍布代碼庫的復(fù)雜事件的混亂局面,這使得在審查或調(diào)試代碼時真的很難發(fā)現(xiàn)問題。
在使用事件總線時,你應(yīng)該注意的另一件大事是與這種機(jī)制的一對多性質(zhì)有關(guān)。相對于監(jiān)聽器的方法,你只有一個事件的訂閱者,在事件總線的方法中,你可能會發(fā)現(xiàn)自己有許多訂閱者,但并不是所有的訂閱者你都知道的。舉例來說,用戶打開了兩個個人資料頁面,都是UserProfileActivity類型的實例。然后,一個事件被觸發(fā)。UserProfileActivity的兩個實例都會收到這個事件,導(dǎo)致其中一個可能被錯誤地更新,因為該事件最初只對應(yīng)于其中一個。這可能是一個錯誤。為了解決這個問題,你可能會發(fā)現(xiàn)自己要走很多彎路,查詢額外的事件屬性,比如用戶的ID,以避免錯誤的事件攔截。
在我看來,事件總線機(jī)制是有道理的,但你應(yīng)該注意在哪些情況下使用它。例如,在應(yīng)用程序交叉事件的情況下,事件的源頭和事件中的角色之間沒有明確的關(guān)系。在基于數(shù)據(jù)變化而更新UI的情況下,比如在我們的例子中,我不認(rèn)為有理由使用事件總線,但在這種方法和之前的監(jiān)聽器接口的方法中,我會選擇后者。
#4 — Using LiveData
在探索了現(xiàn)有的方案來完成這個任務(wù)之后,讓我們看看Android架構(gòu)組件的LiveData是如何解決的。
// IProfileRepository.java
public interface IProfileRepository {
void toggleFollowing();
LiveData<FollowStatus> getFollowStatus();
}
// ProfileRepository.java
public class ProfileRepository implements IProfileRepository {
...
private LiveData<FollowStatus> followStatus;
@Override
public LiveData<FollowStatus> getFollowStatus() {
if (followStatus != null)
return followStatus;
boolean isFollowing = webService.getIsFollowing();
int numberOfFollowers = webService.getNumberOfFollowers();
followStatus = new MutableLiveData<>();
if (isFollowing)
followStatus.setValue(getFollowingStatus(numberOfFollowers));
else
followStatus.setValue(getFollowStatus(numberOfFollowers));
return followStatus;
}
@Override
public void toggleFollowing() {
if (followStatus == null)
return;
webService.toggleFollowing();
int currentNumberOfFollowers = followStatus.getValue().numberOfFollowers;
if (followStatus.isFollowing())
followStatus.setValue(getFollowStatus(numberOfFollowers-1));
else
followStatus.setValue(getFollowingStatus(numberOfFollowers+1));
}
private FollowStatus getFollowingStatus(int numberOfFollowers) {
return new FollowStatus(getLightGreenColor(), "Following", numberOfFollowers);
}
private FollowStatus getFollowStatus(int numberOfFollowers) {
return new FollowStatus(getGreenColor(), "Follow", numberOfFollowers);
}
}
// FollowStatus.java
public class FollowStatus {
public int buttonColor;
public String buttonText;
public int numberOfFollowers;
public FollowStatus(int buttonColor, String buttonText, int numberOfFollowers) {
this.buttonColor = buttonColor;
this.buttonText = buttonText;
this.numberOfFollowers = numberOfFollowers;
}
public boolean isFollowing() {
return "Following".equals(buttonText);
}
}
// ProfileViewModel.java
public class ProfileViewModel extends ViewModel {
public LiveData<FollowStatus> getFollowStatus() {
profileRepo.getFollowStatus();
}
public void toggleFollowing(IProfileListener listener) {
profileRepo.toggleFollowing();
}
}
// UserProfileActivity.java
public class UserProfileActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
profileViewModel = ViewModelProviders.of(this).get(ProfileViewModel.class);
toggleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
profileViewModel.toggleFollowing();
}
});
observeProfile();
}
private void observeProfile() {
profileViewModel.getFollowStatus().observe(this, new Observer<FollowStatus>() {
@Override
public void onChanged(final FollowStatus followStatus) {
toggleButton.setBackground(followStatus.buttonColor);
toggleButton.setText(followStatus.buttonText);
followersText.setText(followStatus.numberOfFollowers + " Followers");
}
});
}
}
好了,讓我們來看看代碼。該Activity創(chuàng)建了ProfileViewModel,它被設(shè)計用來管理和存儲UI相關(guān)的數(shù)據(jù)(或委托其他類存儲)。這個ViewModel(注意,基類屬于Android架構(gòu)組件)的偉大之處在于,它在Activity的生命周期中被保留下來,這意味著它將一直存在,直到Activity永久消失,也就是Activity銷毀。一旦獲取到ProfileViewModel,該Activity就開始觀察數(shù)據(jù)的變化。這就是LiveData的神奇之處。視圖模型返回LiveData,它是一個可觀察的類,從而使我們的Activity成為觀察者。就像基于事件的解決方案一樣,當(dāng)數(shù)據(jù)被改變時,我們會相應(yīng)地改變用戶界面。在我們的例子中,視圖模型從UserRepository類中獲得其返回值,該類保留了一個LiveData的實例,該實例包裹著一個數(shù)據(jù)持有者FollowStatus。為了簡潔起見,我讓存儲庫基于內(nèi)存而不是持久化數(shù)據(jù)。當(dāng)用戶點擊Follow/Unfollow按鈕時,代碼會調(diào)用視圖模型的toggleFollowing方法,這又會調(diào)用UserRepository。一旦存儲庫改變了存儲在其LiveData實例中的FollowStatus值,Activity的onChanged代碼就會被再次調(diào)用,因為Activity會觀察FollowStatus并等待數(shù)據(jù)的改變。這就是數(shù)據(jù)變化<->用戶界面變化周期在LiveData中的工作方式。
LiveData的新特點是它具有生命周期意識。在我們的例子中,它知道我們開始觀察它時給它的這個實例的生命周期,也就是Activity的生命周期。這意味著,只有當(dāng)Activity處于活躍的生命周期狀態(tài)時,LiveData才會發(fā)送一個“on changed event”。例如,如果該Activity是在后臺,它將不會得到數(shù)據(jù)變化的通知,直到它再次對用戶可見。這就意味著不會再有因Activity停止而導(dǎo)致的崩潰了。而且,由于LiveData的可觀察性控制了事件的觸發(fā),我們就不需要手動處理生命周期。這確保了在使用LiveData時,UI組件始終是最新的,即使它在某一時刻變得不活躍,因為它在再次變得活躍時收到最新的數(shù)據(jù)。
LiveData的功能非常強大,以至于有些人使用LiveData實現(xiàn)了事件總線機(jī)制。此外,LiveData還得到了新的SQLite持久化庫Room的支持,該庫是作為Android架構(gòu)組件的一部分推出的。這意味著我們可以將LiveData對象保存到數(shù)據(jù)庫中,之后再將其作為普通的LiveData進(jìn)行觀察。這讓我們可以在代碼中的一個地方保存數(shù)據(jù),并讓另一個地方的代碼,觀察它數(shù)據(jù)的改變。我們可以擴(kuò)展我們的UserRepository來使用Room并持久化數(shù)據(jù),但我不想過度擴(kuò)展這個例子。
Summary
在回顧了解決同一任務(wù)的不同方法后,我們可以把LiveData看作是界面監(jiān)聽器和基于事件的解決方案的混合體,從每個解決方案中吸取精華。作為一個經(jīng)驗法則,我建議在幾乎所有考慮過(或已經(jīng)使用過)其他替代方案的情況下都使用(或切換到)LiveData,特別是在我們希望以干凈、穩(wěn)健和合理的方式根據(jù)數(shù)據(jù)變化更新用戶界面的所有場景中。
我希望你能從這篇文章中獲得一些關(guān)于LiveData的知識,了解它在哪些情況下可以提供幫助,如何使用它,以及為什么它可能是一個比其他現(xiàn)有方法更好的解決方案。有其他想法嗎?有更好的解決方案嗎?請隨時發(fā)表評論。
When to load data in ViewModels
最近,我對一個表面上很簡單的問題進(jìn)行了出乎意料的長時間討論。在我們的代碼中,我們究竟應(yīng)該在哪里觸發(fā)ViewModel數(shù)據(jù)的加載。有許多可能的選擇,但讓我們看一下其中的幾個。
兩年多前,為了改善我們開發(fā)應(yīng)用程序的方式,架構(gòu)組件被引入到Android世界。這些組件的一個核心部分是帶有LiveData的ViewModel,它是一個可觀察到的生命周期感知的數(shù)據(jù)持有者,用于連接Activity和ViewModel。ViewModel輸出數(shù)據(jù),Activities消費數(shù)據(jù)。
這一部分很清楚,不會引起太多的討論,但是ViewModel必須在某個時候加載、訂閱或觸發(fā)其數(shù)據(jù)的加載。問題是,這應(yīng)該在什么時候進(jìn)行。
Our Use Case
對于我們的討論,讓我們使用一個簡單的用例,在我們的ViewModel中加載一個聯(lián)系人列表,并使用LiveData發(fā)布它。
class Contacts(val names: List<String>)
data class Parameters(val namePrefix: String = "")
class GetContactsUseCase {
fun loadContacts(parameters: Parameters, onLoad: (Contacts) -> Unit) { /* Implementation detail */ }
}
class ContactsViewModel(val getContactsUseCase: GetContactsUseCase) : ViewModel() {
// TODO When to call getContactsUseCase.loadContacts?
fun contacts(parameters: Parameters): LiveData<Contacts> {
TODO("What to return here?")
}
}
What do we want
為了有一些評估的標(biāo)準(zhǔn),讓我們首先假定下,我們對有效的加載技術(shù)的要求。
利用ViewModel的優(yōu)勢,只在需要的時候加載,與生命周期的改變和配置的變化脫鉤。 易于理解和實現(xiàn),使用干凈的代碼架構(gòu)。 小型API以減少使用ViewModel所需的知識。 有可能提供參數(shù)。ViewModel很多時候需要接受參數(shù)來加載其數(shù)據(jù)。
? Bad: Calling a method
這是一個被廣泛使用的概念,甚至在Google Blueprints的例子中也得到了推廣,但它有很大的問題。該方法需要從某個地方被調(diào)用,而這通常會在Activity或Fragment的某個生命周期方法中結(jié)束。
class ContactsViewModel(val getContactsUseCase: GetContactsUseCase) : ViewModel() {
private val contactsLiveData = MutableLiveData<Contacts>()
fun loadContacts(parameters: Parameters) {
getContactsUseCase.loadContacts(parameters) { contactsLiveData.value = it }
}
fun contacts(): LiveData<Contacts> = contactsLiveData
}
?我們在每次旋轉(zhuǎn)時重新加載,失去了與Activity/Fragment生命周期解耦的好處,因為他們必須從onCreate()或其他生命周期方法中調(diào)用該方法。 ?易于實現(xiàn)和理解。 ?多了一個觸發(fā)的方法。 ?引入隱含條件,即參數(shù)對同一實例總是相同的。loadContacts()和contacts()方法是耦合的。 ?容易提供參數(shù)。
? Bad: Start in ViewModel constructor
我們可以通過在ViewModel的構(gòu)造函數(shù)中觸發(fā)加載,輕松確保數(shù)據(jù)只被加載一次。這種方法在文檔中也有顯示。
class ContactsViewModel(val getContactsUseCase: GetContactsUseCase) : ViewModel() {
private val contactsLiveData = MutableLiveData<Contacts>()
init {
getContactsUseCase.loadContacts(Parameters()) { contactsLiveData.value = it }
}
fun contacts(): LiveData<Contacts> = contactsLiveData
}
?我們只加載一次數(shù)據(jù)。 ?容易實現(xiàn)。
整個公共API是一個方法contacts()
?不可能為加載函數(shù)提供參數(shù)。 ?我們在構(gòu)造函數(shù)中進(jìn)行工作。
?? Better: Lazy field
我們可以使用Kotlin的lazy委托屬性功能,比如下面的代碼。
class ContactsViewModel(val getContactsUseCase: GetContactsUseCase) : ViewModel() {
private val contactsLiveData by lazy {
val liveData = MutableLiveData<Contacts>()
getContactsUseCase.loadContacts(Parameters()) { liveData.value = it }
return@lazy liveData
}
fun contacts(): LiveData<Contacts> = contactsLiveData
}
?我們只在第一次訪問LiveData的時候加載數(shù)據(jù)。 ?容易實現(xiàn)。
整個公共API是一個方法 contacts()。除了增加一個狀態(tài)外,這個方案不可能為加載函數(shù)提供參數(shù),這個參數(shù)必須在訪問 contactsLiveData字段前設(shè)置。
?? Good: Lazy Map
我們可以根據(jù)提供的參數(shù)使用lazyMap或類似的lazy的init。當(dāng)參數(shù)是字符串或其他不可變的類時,很容易將它們作為Map的鍵來獲取與所提供的參數(shù)對應(yīng)的LiveData。
class ContactsViewModel(val getContactsUseCase: GetContactsUseCase) : ViewModel() {
private val contactsLiveData: Map<Parameters, LiveData<Contacts>> = lazyMap { parameters ->
val liveData = MutableLiveData<Contacts>()
getContactsUseCase.loadContacts(parameters) { liveData.value = it }
return@lazyMap liveData
}
fun contacts(parameters: Parameters): LiveData<Contacts> = contactsLiveData.getValue(parameters)
}
lazyMap的定義如下所示。
fun <K, V> lazyMap(initializer: (K) -> V): Map<K, V> {
val map = mutableMapOf<K, V>()
return map.withDefault { key ->
val newValue = initializer(key)
map[key] = newValue
return@withDefault newValue
}
}
?我們只在第一次訪問LiveData的時候加載數(shù)據(jù)。 ?相當(dāng)容易實現(xiàn)和理解。
整個公共API是一個方法 contacts()
?我們可以提供參數(shù),ViewModel甚至可以同時處理多個參數(shù)。 ?仍然在ViewModel中保留一些可變的狀態(tài)。
?? Good: Library method — Lazy onActive() case
當(dāng)使用Room或RxJava時,它們有適配器,能夠直接在@Dao對象中創(chuàng)建LiveData,分別使用Publisher.toLiveData()的擴(kuò)展方法。
這兩個庫的實現(xiàn)ComputableLiveData和PublisherLiveData都是lazy的,即當(dāng)LiveData.onActive()方法被調(diào)用時,它們會進(jìn)行工作。
class GetContactsUseCase {
fun loadContacts(parameters: Parameters): Flowable<Contacts> { /* Implementation detail */ }
}
class ContactsViewModel(val getContactsUseCase: GetContactsUseCase) : ViewModel() {
fun contacts(parameters: Parameters): LiveData<Contacts> {
return getContactsUseCase.loadContacts(parameters).toLiveData()
}
}
?只有當(dāng)生命周期處于活動狀態(tài)時,我們才會懶惰地加載數(shù)據(jù)。 ?加載仍然與生命周期耦合,因為LiveData.onActive()基本上意味著(onStart()并有觀察者)。
易于實現(xiàn),并使用支持庫。整個公共API是一個方法 contacts()
在這個例子中,我們?yōu)槊總€方法的調(diào)用創(chuàng)建了新的LiveData,為了避免這種情況,我們必須解決參數(shù)可能不同的問題。Lazy Map在這里可以提供幫助。這里有一個例子。
?? Good: Pass the parameters in constructor
在前面的案例中,我們使用LazyMap選項,只是為了能夠傳遞參數(shù),但在很多情況下,ViewModel的一個實例總是有相同的參數(shù)。
讓參數(shù)傳遞給構(gòu)造函數(shù)并使用lazy加載或在構(gòu)造函數(shù)中開始加載會好得多。我們可以使用ViewModelProvider.Factory來實現(xiàn)這一點,但它會有一些問題。
class ContactsViewModel(val getContactsUseCase: GetContactsUseCase, parameters: Parameters) : ViewModel() {
private val contactsLiveData: LiveData<Contacts> by lazy {
val liveData = MutableLiveData<Contacts>()
getContactsUseCase.loadContacts(parameters) { liveData.value = it }
return@lazy liveData
}
fun contacts(parameters: Parameters): LiveData<Contacts> = contactsLiveData
}
class ContactsViewModelFactory(val getContactsUseCase: GetContactsUseCase, val parameters: Parameters)
: ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ContactsViewModel(getContactsUseCase, parameters) as T
}
}
?我們只加載一次數(shù)據(jù)。 ?實現(xiàn)和理解起來并不容易,需要模板。
整個公共API是一個方法 contacts()
?ViewModel在構(gòu)造函數(shù)中接受參數(shù),不可改變,可測試性強。
這需要額外的代碼來鉤住ViewModelFactory,以便我們可以傳遞動態(tài)參數(shù)。同時,我們開始遇到其他依賴關(guān)系的問題,我們需要弄清楚如何將它們和參數(shù)一起傳入工廠,從而產(chǎn)生更多的模板。
Assisted Injection正試圖解決這個問題,Jake Wharton在Droidcon London 2018的演講中談到了這個話題。然而,仍然有一些模板代碼,因此,即使這可能是“完美”的解決方案,比其他選項可能更適合你的團(tuán)隊。
Which approach to choose
架構(gòu)組件的引入大大簡化了Android的開發(fā),解決了許多問題。盡管如此,仍然有一些問題,我們在這里討論了加載ViewModel數(shù)據(jù)和評估各種選項的問題。
根據(jù)我的經(jīng)驗,我推薦LazyMap方法,因為我發(fā)現(xiàn)它很好地平衡了優(yōu)點和缺點,而且真的很容易采用。你可以在這里找到例子。
https://github.com/jraska/github-client/blob/7748c6ae80f07f4d0642ec38775444cb61338792/feature-users/src/main/java/com/jraska/github/client/users/model/RepoDetailViewModel.kt
正如你所看到的,沒有完美的解決方案,要由你的團(tuán)隊來選擇最適合你的方法,平衡健壯性、簡單性和整個項目的一致性。希望這篇文章能幫助你選擇。編碼愉快!
When NOT to Use LiveData
如果你熟悉Android開發(fā),我毫不懷疑你已經(jīng)聽說過架構(gòu)組件,甚至可能在你的項目中使用了它們。有幾篇文章在談?wù)摵螘r和如何使用它們,但我覺得對何時不使用它們強調(diào)得不夠,特別是考慮到谷歌的應(yīng)用程序架構(gòu)指南將它們作為一個相當(dāng)通用的工具,可以在你的架構(gòu)的所有層上使用。因此,肯定會有一種試圖最大限度地利用它們的誘惑:)
在這篇文章中,我將談?wù)勗谑裁辞闆r下我不推薦使用LiveData,以及你可以使用的替代方案。這篇文章的靈感來自于18年安卓開發(fā)峰會上的一個演講,我覺得這個演講很新穎,很有趣。
1. You have backpressure in your app.
如果你有一個實際的Stream,它可能發(fā)生背壓的問題,那么LiveData就不能解決你的問題。原因是LiveData并不支持它。LiveData的目的是在觀察者處于/進(jìn)入活動狀態(tài)時向UI推送最新的值。你可以使用RX Flowable或Kotlin的Flow來正確處理這個問題。下面的圖片展示了背壓的正確處理。在你使用LiveData的情況下,9,10,11的值將被丟棄,以提供最新的值。這里有一個谷歌問題追蹤器的鏈接,確認(rèn)LiveData不能處理背壓。

2. You need to use a lot of operators on data.
即使LiveData提供了Transformations這樣的工具,它也只有map和switchMap可以幫助你開箱即用。如果你想要更高級的東西,而且可能是鏈?zhǔn)降?,LiveData并沒有提供這種數(shù)據(jù)操作。因此,處理這種需求的最好方法是不使用LiveData作為生產(chǎn)者,而是使用RX類型或Kotlin,因為Kotlin支持多種高階函數(shù)以及對Collections和Sequence的擴(kuò)展。下面是一些例子,說明在Kotlin中使用高階函數(shù)可以避免多少模板。
fruits
.filter { it.type == "apple" }
.firstOrNull { it.color == "orange" }
這里有一個switchMap變換的例子。
val result: LiveData<String> = Transformations
.switchMap(firstRepo.getData(), {
if (it.type == "apple") {
MutableLiveData().apply { setValue(it) }
}
})
3. You don’t have UI interactions with data.
如果你不把數(shù)據(jù)傳播到用戶界面,那么使用生命周期感知組件就沒有意義了。LiveData的主要目的是在組件的生命周期中保持?jǐn)?shù)據(jù)狀態(tài)。如果數(shù)據(jù)只是在后臺緩存或同步,你可以使用回調(diào)、RX類型或其他類型或異步操作。

4. You have one-shot asynchronous operations.
如果你不需要觀察數(shù)據(jù)的變化并將其傳播到感知生命周期變化的用戶界面(正如我們在#3中討論的那樣)中,那就沒有必要使用LiveData。你可以使用RX或Kotlin的coroutines對操作者和線程控制進(jìn)行更有力的控制。LiveData并不能對你的線程管理提供完全的控制權(quán)。LiveData基本上有兩種選擇:同步更新或從工作線程發(fā)布異步值。這也可以說是一種優(yōu)勢,如果你不需要完全的控制,而只是知道變化會命中UI線程,而不需要任何額外的切換線程的步驟,這聽起來像是在某些情況下的一種優(yōu)勢。
5. You don’t need to persist cached data into UI.
使用LiveData的所有優(yōu)點在某種程度上都與LiveData的生命周期意識有關(guān)。它允許你通過配置的變化來持久化UI狀態(tài)。如果不需要持久化數(shù)據(jù),那么在你的使用案例中,LiveData將無法實現(xiàn)其目的。

我們已經(jīng)簡要介紹了在哪些用例中使用LiveData是不合理的,甚至可能對你的功能和可擴(kuò)展性造成一些限制。根據(jù)事物的用途來使用它們會更容易,讓你最大限度地發(fā)揮它們提供的優(yōu)勢。LiveData被特意創(chuàng)建為一個數(shù)據(jù)持有者,通過配置的變化來保持?jǐn)?shù)據(jù),充分利用它的生命周期意識會給你的Android項目帶來很多好處,但期望超過它所能提供的,會讓你陷入用勺子吃牛排的境地 :) 編碼愉快 :)
原文鏈接:
https://proandroiddev.com/when-and-why-to-use-android-livedata-93d7dd949138
https://proandroiddev.com/when-to-load-data-in-viewmodels-ad9616940da7
https://proandroiddev.com/when-not-to-use-livedata-6a1245b054a6
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達(dá)
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
