【译】LiveData三连

共 29960字,需浏览 60分钟

 ·

2021-11-27 17:09

点击上方蓝字关注我,知识会给你力量


这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。

When and why to use Android LiveData

差不多一年前(2017年5月的第一个alpha版本),谷歌发布了 "安卓架构组件",这是一个库的集合,旨在帮助安卓开发人员设计更强大、可测试和可维护的应用程序。最引人注目的是LiveData类和相关的生命周期感知类、Room持久性库和新的分页库。在这篇文章中,我将探讨LiveData类,它期望希望解决的问题以及何时去使用这个库。

老实说,LiveData是一个可观察的数据持有者。它让你的应用程序中的组件,通常是UI,能够观察LiveData对象的变化。

关于这个LiveData的新概念是,它具有生命周期意识,这意味着它尊重应用程序组件(Activity、Fragment)的生命周期状态,并确保LiveData只在组件(观察者)处于活跃的生命周期状态时更新它。这种行为可以防止内存泄漏,确保应用程序不会做更多无效的工作。

为了更好地理解何时使用这个新的可观察的数据持有者以及使用它的优势,在这篇文章的其余部分,我将回顾一些替代方案,以面对根据数据变化更新UI这一基本任务。

  • 在UI组件中管理数据
  • 使用一个监听器接口
  • 使用事件总线
  • 使用LiveData
  • 总结

但首先,让我们介绍一下我们的示例方案。

Scenario

为了用代码片段进行演示,我们想象一下,构建一个社交网络应用中的界面UI,它显示了一个用户的简介以及该用户的关注者数量。在简介图片和当前关注者数量的下方,有一个切换按钮,让当前登录的用户可以关注/取消关注该用户。我们希望这个按钮能够影响带有关注者数量的标签,并相应地改变按钮上的文字。(代码将使用Java语言)。

img

#1 — Managing data in the UI component

一个天真的想法是将UI组件(Activity、Fragment)变成“上帝对象”。你首先从框架提供给你的唯一的UI组件开始,比如说一个Activity,你在那里写你的应用程序的UI代码。后来,当你需要处理数据并在此基础上改变UI时,你会发现继续在活动中写代码,这样会更容易,因为它已经包含了所有需要更新的字段和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());
    }
}

这种方式是一种反面模式,它违反了编写良好设计的代码的一些关键原则,当涉及到数据和持久性时,这种方法还有一个重大缺陷。

那就是数据和状态的丢失——像Activity或Fragment这样的应用程序组件不是由我们管理,而是由系统管理。因为它们的生命周期不在我们的控制之下,它们可以在任何时候根据用户的互动或其他因素(如低内存)被销毁。如果我们在一个UI组件中创建和处理我们的数据,一旦该组件被销毁,我们所有的数据都会被销毁。在这个例子中,例如每次用户旋转设备时,该Activity就会被销毁并重新创建,导致所有的数据被重置,网络调用再次被执行,浪费了用户的流量,迫使用户等待新的查询完成等操作。

#2 — Using a listener interface

解决这个基于数据变化更新UI的任务的另一种方法是,使用监听器接口,它给UI监听器施加了一个特定的功能。

// 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");
  }
}

为了简洁起见,我省略了一些接口的声明和一些实现。首先,我们来看看这个解决方案。Activity本身并没有意识到用户关注者的数据信息变化。它唯一关心的是显示一个带有文本的用户界面,用户可以在那里点击一个按钮。请注意,现在的Activity不包含任何一行if条件代码。Activity根据ProfileController来获取数据。ProfileController反过来使用ProfileRepository来获取数据,无论是从网络(使用先前在Activity中使用的WebService)还是从其他地方(如内存缓存或持久化)。一旦ProfileController得到数据并准备好更新用户界面,它就会回调传入的监听器(实际上是Activity)并调用它的一个方法。实际上,ProfileController是迈向MVP设计(Model-View-Presenter)的第一步。我们可以将Controller设置为使用更多的迷你Controller,每个Controller都会自己改变相应的UI元素,从而将改变UI的功能完全从活动中提取出来。

这种方案避免了UI组件被破坏后的数据丢失问题,对于正确分离代码中的关注点很有用。此外,它使UI组件的代码保持干净和尽可能的精简,从而使我们的代码更容易维护,并且一般来说,我们可以避免许多与生命周期有关的问题。但这种有效方法的主要缺点是,它有些容易出错,如果你不够小心,你会发现自己造成了一个异常或崩溃。这个简单的例子有点难以证明,但对于更复杂和真实的场景,错误是一定会发生的。例如,如果Activity经历了配置的改变,你的监听器引用可能是空的。另一个例子是,当你的监听器的生命周期是不活跃的,比如在后堆栈中的Activity,但你依然试图将事件传递给它并调用它的功能。一般来说,这种方法要求你了解监听器(UI组件)的生命周期,并在你的代码中考虑到它。对于像Kotlin这样函数是一等公民的语言来说也是如此。尽管你可以将一个函数作为参数而不是UI组件本身传递,但在这里你也应该知道UI组件的生命周期,因为该函数通常会操作该组件的UI元素。

#3 — Using an event bus

另一种方法是,当我们必须根据数据变化更新用户界面时,使用基于事件的机制(发布者/订阅者)(使用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);
  }
}

正如你所看到的,这个解决方案与之前的解决方案非常相似。所不同的是,我们不是调用监听器的方法,而是触发事件。这些事件被订阅者拦截,在我们的例子中就是Activity,然后用户界面就会相应地改变。

在社区内有一个激烈的讨论,即事件总线是否是一个好的解决方案,或者说监听器回调是否是真正的解决方案。无论如何,这种技术,作为监听器接口,也避免了数据丢失,并保持代码中的职责分离。此外,实现基于事件机制的库通常支持高级功能,如交付线程和订阅者的优先级(人们也可以使用Android LocalBroadcast,而不需要第三方库)。相对于监听器接口的方法,基于事件的方法不需要你考虑生命周期的问题,因为大多数库都会为你考虑这一点。然而,你需要注意注册(和取消注册)订阅者,以便他们能够接收事件,如果不能正确地这样做,可能会导致不被注意的内存泄漏。此外,尽管事件总线一开始看起来很方便实现,但它很快就会变成一个遍布代码库的复杂事件的混乱局面,这使得在审查或调试代码时真的很难发现问题。

在使用事件总线时,你应该注意的另一件大事是与这种机制的一对多性质有关。相对于监听器的方法,你只有一个事件的订阅者,在事件总线的方法中,你可能会发现自己有许多订阅者,但并不是所有的订阅者你都知道的。举例来说,用户打开了两个个人资料页面,都是UserProfileActivity类型的实例。然后,一个事件被触发。UserProfileActivity的两个实例都会收到这个事件,导致其中一个可能被错误地更新,因为该事件最初只对应于其中一个。这可能是一个错误。为了解决这个问题,你可能会发现自己要走很多弯路,查询额外的事件属性,比如用户的ID,以避免错误的事件拦截。

在我看来,事件总线机制是有道理的,但你应该注意在哪些情况下使用它。例如,在应用程序交叉事件的情况下,事件的源头和事件中的角色之间没有明确的关系。在基于数据变化而更新UI的情况下,比如在我们的例子中,我不认为有理由使用事件总线,但在这种方法和之前的监听器接口的方法中,我会选择后者。

#4 — Using LiveData

在探索了现有的方案来完成这个任务之后,让我们看看Android架构组件的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创建了ProfileViewModel,它被设计用来管理和存储UI相关的数据(或委托其他类存储)。这个ViewModel(注意,基类属于Android架构组件)的伟大之处在于,它在Activity的生命周期中被保留下来,这意味着它将一直存在,直到Activity永久消失,也就是Activity销毁。一旦获取到ProfileViewModel,该Activity就开始观察数据的变化。这就是LiveData的神奇之处。视图模型返回LiveData,它是一个可观察的类,从而使我们的Activity成为观察者。就像基于事件的解决方案一样,当数据被改变时,我们会相应地改变用户界面。在我们的例子中,视图模型从UserRepository类中获得其返回值,该类保留了一个LiveData的实例,该实例包裹着一个数据持有者FollowStatus。为了简洁起见,我让存储库基于内存而不是持久化数据。当用户点击Follow/Unfollow按钮时,代码会调用视图模型的toggleFollowing方法,这又会调用UserRepository。一旦存储库改变了存储在其LiveData实例中的FollowStatus值,Activity的onChanged代码就会被再次调用,因为Activity会观察FollowStatus并等待数据的改变。这就是数据变化<->用户界面变化周期在LiveData中的工作方式。

LiveData的新特点是它具有生命周期意识。在我们的例子中,它知道我们开始观察它时给它的这个实例的生命周期,也就是Activity的生命周期。这意味着,只有当Activity处于活跃的生命周期状态时,LiveData才会发送一个“on changed event”。例如,如果该Activity是在后台,它将不会得到数据变化的通知,直到它再次对用户可见。这就意味着不会再有因Activity停止而导致的崩溃了。而且,由于LiveData的可观察性控制了事件的触发,我们就不需要手动处理生命周期。这确保了在使用LiveData时,UI组件始终是最新的,即使它在某一时刻变得不活跃,因为它在再次变得活跃时收到最新的数据。

LiveData的功能非常强大,以至于有些人使用LiveData实现了事件总线机制。此外,LiveData还得到了新的SQLite持久化库Room的支持,该库是作为Android架构组件的一部分推出的。这意味着我们可以将LiveData对象保存到数据库中,之后再将其作为普通的LiveData进行观察。这让我们可以在代码中的一个地方保存数据,并让另一个地方的代码,观察它数据的改变。我们可以扩展我们的UserRepository来使用Room并持久化数据,但我不想过度扩展这个例子。

Summary

在回顾了解决同一任务的不同方法后,我们可以把LiveData看作是界面监听器和基于事件的解决方案的混合体,从每个解决方案中吸取精华。作为一个经验法则,我建议在几乎所有考虑过(或已经使用过)其他替代方案的情况下都使用(或切换到)LiveData,特别是在我们希望以干净、稳健和合理的方式根据数据变化更新用户界面的所有场景中。

我希望你能从这篇文章中获得一些关于LiveData的知识,了解它在哪些情况下可以提供帮助,如何使用它,以及为什么它可能是一个比其他现有方法更好的解决方案。有其他想法吗?有更好的解决方案吗?请随时发表评论。

When to load data in ViewModels

最近,我对一个表面上很简单的问题进行了出乎意料的长时间讨论。在我们的代码中,我们究竟应该在哪里触发ViewModel数据的加载。有许多可能的选择,但让我们看一下其中的几个。

两年多前,为了改善我们开发应用程序的方式,架构组件被引入到Android世界。这些组件的一个核心部分是带有LiveData的ViewModel,它是一个可观察到的生命周期感知的数据持有者,用于连接Activity和ViewModel。ViewModel输出数据,Activities消费数据。

这一部分很清楚,不会引起太多的讨论,但是ViewModel必须在某个时候加载、订阅或触发其数据的加载。问题是,这应该在什么时候进行。

Our Use Case

对于我们的讨论,让我们使用一个简单的用例,在我们的ViewModel中加载一个联系人列表,并使用LiveData发布它。

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

为了有一些评估的标准,让我们首先假定下,我们对有效的加载技术的要求。

  • 利用ViewModel的优势,只在需要的时候加载,与生命周期的改变和配置的变化脱钩。
  • 易于理解和实现,使用干净的代码架构。
  • 小型API以减少使用ViewModel所需的知识。
  • 有可能提供参数。ViewModel很多时候需要接受参数来加载其数据。

❌ Bad: Calling a method

这是一个被广泛使用的概念,甚至在Google Blueprints的例子中也得到了推广,但它有很大的问题。该方法需要从某个地方被调用,而这通常会在Activity或Fragment的某个生命周期方法中结束。

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
}
  • ➖我们在每次旋转时重新加载,失去了与Activity/Fragment生命周期解耦的好处,因为他们必须从onCreate()或其他生命周期方法中调用该方法。
  • ➕易于实现和理解。
  • ➖多了一个触发的方法。
  • ➖引入隐含条件,即参数对同一实例总是相同的。loadContacts()和contacts()方法是耦合的。
  • ➕容易提供参数。

❌ Bad: Start in ViewModel constructor

我们可以通过在ViewModel的构造函数中触发加载,轻松确保数据只被加载一次。这种方法在文档中也有显示。

class ContactsViewModel(val getContactsUseCase: GetContactsUseCase) : ViewModel() {
  private val contactsLiveData = MutableLiveData<Contacts>()

  init {
    getContactsUseCase.loadContacts(Parameters()) { contactsLiveData.value = it }
  }

  fun contacts(): LiveData<Contacts> = contactsLiveData
}
  • ➕我们只加载一次数据。
  • ➕容易实现。

整个公共API是一个方法contacts()

  • ➖不可能为加载函数提供参数。
  • ➖我们在构造函数中进行工作。

✔️ 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的时候加载数据。
  • ➕容易实现。

整个公共API是一个方法 contacts()。除了增加一个状态外,这个方案不可能为加载函数提供参数,这个参数必须在访问 contactsLiveData字段前设置。

✔️ Good: Lazy Map

我们可以根据提供的参数使用lazyMap或类似的lazy的init。当参数是字符串或其他不可变的类时,很容易将它们作为Map的键来获取与所提供的参数对应的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的时候加载数据。
  • ➕相当容易实现和理解。

整个公共API是一个方法 contacts()

  • ➕我们可以提供参数,ViewModel甚至可以同时处理多个参数。
  • ➖仍然在ViewModel中保留一些可变的状态。

✔️ Good: Library method — Lazy onActive() case

当使用Room或RxJava时,它们有适配器,能够直接在@Dao对象中创建LiveData,分别使用Publisher.toLiveData()的扩展方法。

这两个库的实现ComputableLiveData和PublisherLiveData都是lazy的,即当LiveData.onActive()方法被调用时,它们会进行工作。

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()
  }
}
  • ➕只有当生命周期处于活动状态时,我们才会懒惰地加载数据。
  • ➖加载仍然与生命周期耦合,因为LiveData.onActive()基本上意味着(onStart()并有观察者)。

易于实现,并使用支持库。整个公共API是一个方法 contacts()

在这个例子中,我们为每个方法的调用创建了新的LiveData,为了避免这种情况,我们必须解决参数可能不同的问题。Lazy Map在这里可以提供帮助。这里有一个例子。

✔️ Good: Pass the parameters in constructor

在前面的案例中,我们使用LazyMap选项,只是为了能够传递参数,但在很多情况下,ViewModel的一个实例总是有相同的参数。

让参数传递给构造函数并使用lazy加载或在构造函数中开始加载会好得多。我们可以使用ViewModelProvider.Factory来实现这一点,但它会有一些问题。

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
  }
}
  • ➕我们只加载一次数据。
  • ➖实现和理解起来并不容易,需要模板。

整个公共API是一个方法 contacts()

  • ➕ViewModel在构造函数中接受参数,不可改变,可测试性强。

这需要额外的代码来钩住ViewModelFactory,以便我们可以传递动态参数。同时,我们开始遇到其他依赖关系的问题,我们需要弄清楚如何将它们和参数一起传入工厂,从而产生更多的模板。

Assisted Injection正试图解决这个问题,Jake Wharton在Droidcon London 2018的演讲中谈到了这个话题。然而,仍然有一些模板代码,因此,即使这可能是“完美”的解决方案,比其他选项可能更适合你的团队。

Which approach to choose

架构组件的引入大大简化了Android的开发,解决了许多问题。尽管如此,仍然有一些问题,我们在这里讨论了加载ViewModel数据和评估各种选项的问题。

根据我的经验,我推荐LazyMap方法,因为我发现它很好地平衡了优点和缺点,而且真的很容易采用。你可以在这里找到例子。

https://github.com/jraska/github-client/blob/7748c6ae80f07f4d0642ec38775444cb61338792/feature-users/src/main/java/com/jraska/github/client/users/model/RepoDetailViewModel.kt

正如你所看到的,没有完美的解决方案,要由你的团队来选择最适合你的方法,平衡健壮性、简单性和整个项目的一致性。希望这篇文章能帮助你选择。编码愉快!

When NOT to Use LiveData

如果你熟悉Android开发,我毫不怀疑你已经听说过架构组件,甚至可能在你的项目中使用了它们。有几篇文章在谈论何时和如何使用它们,但我觉得对何时不使用它们强调得不够,特别是考虑到谷歌的应用程序架构指南将它们作为一个相当通用的工具,可以在你的架构的所有层上使用。因此,肯定会有一种试图最大限度地利用它们的诱惑:)

在这篇文章中,我将谈谈在什么情况下我不推荐使用LiveData,以及你可以使用的替代方案。这篇文章的灵感来自于18年安卓开发峰会上的一个演讲,我觉得这个演讲很新颖,很有趣。

1. You have backpressure in your app.

如果你有一个实际的Stream,它可能发生背压的问题,那么LiveData就不能解决你的问题。原因是LiveData并不支持它。LiveData的目的是在观察者处于/进入活动状态时向UI推送最新的值。你可以使用RX Flowable或Kotlin的Flow来正确处理这个问题。下面的图片展示了背压的正确处理。在你使用LiveData的情况下,9,10,11的值将被丢弃,以提供最新的值。这里有一个谷歌问题追踪器的链接,确认LiveData不能处理背压。

img

2. You need to use a lot of operators on data.

即使LiveData提供了Transformations这样的工具,它也只有map和switchMap可以帮助你开箱即用。如果你想要更高级的东西,而且可能是链式的,LiveData并没有提供这种数据操作。因此,处理这种需求的最好方法是不使用LiveData作为生产者,而是使用RX类型或Kotlin,因为Kotlin支持多种高阶函数以及对Collections和Sequence的扩展。下面是一些例子,说明在Kotlin中使用高阶函数可以避免多少模板。

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.

如果你不把数据传播到用户界面,那么使用生命周期感知组件就没有意义了。LiveData的主要目的是在组件的生命周期中保持数据状态。如果数据只是在后台缓存或同步,你可以使用回调、RX类型或其他类型或异步操作。

img

4. You have one-shot asynchronous operations.

如果你不需要观察数据的变化并将其传播到感知生命周期变化的用户界面(正如我们在#3中讨论的那样)中,那就没有必要使用LiveData。你可以使用RX或Kotlin的coroutines对操作者和线程控制进行更有力的控制。LiveData并不能对你的线程管理提供完全的控制权。LiveData基本上有两种选择:同步更新或从工作线程发布异步值。这也可以说是一种优势,如果你不需要完全的控制,而只是知道变化会命中UI线程,而不需要任何额外的切换线程的步骤,这听起来像是在某些情况下的一种优势。

5. You don’t need to persist cached data into UI.

使用LiveData的所有优点在某种程度上都与LiveData的生命周期意识有关。它允许你通过配置的变化来持久化UI状态。如果不需要持久化数据,那么在你的使用案例中,LiveData将无法实现其目的。

img

我们已经简要介绍了在哪些用例中使用LiveData是不合理的,甚至可能对你的功能和可扩展性造成一些限制。根据事物的用途来使用它们会更容易,让你最大限度地发挥它们提供的优势。LiveData被特意创建为一个数据持有者,通过配置的变化来保持数据,充分利用它的生命周期意识会给你的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

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报