LiveData && ViewModel 使用详解

33次阅读

共计 12896 个字符,预计需要花费 33 分钟才能阅读完成。

前言
在之前的文章中,我们讲了 Android Architecture components 中的 Lifecycle 组件的详细使用以及源码解析。本篇将介绍另外 AAC 中另外两个组件:LiveData 和 ViewModel,它们的实现也都是利用了 Lifecycle。
什么是 LiveData
LiveData 是一个可观测的数据持有类,但是不同于通常的被观察者,LiveData 具有生命周期感知能力。通俗点说,LiveData 就是具有“Live”能力的“Data”持有类。当它所持有的数据发生改变的时候,并且 Lifecycle 对象 (比如 Activity 或者 Fragment 等) 处于活跃状态(STARTED 或者 RESUMED),LiveData 将立即通知观察者数据发生了变化。也就是说,比普通观察者多了个生命周期感知能力。
LiveData 的优势
确保 UI 和数据状态匹配。
当数据发生改变的时候,会自动通知 UI 进行更新。
避免内存泄漏
Observers 是绑定到 Lifecycle 对象上的,当与其关联的 lifecycle 被销毁的时候,它们会自动被清理。
避免了由于 Activity 停止而导致的闪退
当 Observer 所绑定的 Lifecycle 处于非活跃状态时,比如处于返回栈中的 Activity,它将不会收到任何 LiveData 事件。
不再需要手动处理生命周期
UI 组件只需要对相关的数据进行监听,不需要关心是否应该暂停或者恢复监听。LiveData 具有生命周期感知能力,它会自动对这些进行管理。
数据总处于最新状态
如果一个 Lifecycle 处于非活跃状态,那当它由非活跃状态变为活跃状态的时候,它将收到最新的数据。比如一个 Activity 由后台转为前台,这时候它将立即收到最新的数据
系统配置更改时,进行数据的保存和恢复,及 UI 的恢复。
当 Activity 或者 Fragment 由于配置更改而重新创建时(比如旋转屏幕等),它将收到最新的可用数据。这里简单提一点,这个有点是需要配合 ViewModel 使用的,严格来说,它主要是 ViewModel 的优点
资源共享
我们可以使用单例模式来扩展 LiveData,这样就能达到数据变化的时候,通知所有的观察者。
为了便于理解,关于 LiveData 和 ViewModel 的关系,我这里先说结论:
LiveData 的作用是在使得数据能具有生命周期感知能力,在 Activity 等变为活跃状态的时候,自动回调观察者中的回调方法。也就是说对数据的变化进行实时监听。而 ViewModel 的作用则是,当因系统配置发生改变导致 Activity 重建的时候(比如旋转屏幕),能对 LiveData 进行正确的保存和恢复。仅此而已。
LiveData 的使用
一般来讲,LiveData 是需要配合 ViewModel 来使用的,但千万不要觉得 LiveData 就一定结合 ViewModel。上面也说道二者只是功能互补。这里为了便于理解,我们先单独学习下 LiveData 的使用。
LiveData 的使用分三步:

创建一个 LiveData 的实例,让它持有一种特定的数据类型,比如 String 或者 User . 通常是将 LiveData 放在 ViewModel 中使用的(这里我们先单独使用)。
创建一个 Observer 对象,并实现其 onChanged(…) 方法,在这里定义当 LiveData 持有的数据发生改变的时候,应该做何操作。可以在这进行 UI 的更新,一般 Observer 是在 UI controller 中创建,比如 Activity 或者 Fragment。
通过创建的 LiveData 实例的 observe(…)方法,将 Observer 对象添加进 LiveData 中。方法的原型为 observe(LifecycleOwner owner, Observer<? super T> observer),第一个参数是 LifecycleOwner 对象,这也是 LiveData 能监听生命周期的能力来源。第二个参数就是我们的监听器对象 Observer。

添加 LiveData 和 ViewModel 的依赖:
implementation “android.arch.lifecycle:extensions:1.1.1”
当然,你也可以分别单独集成 LiveData 和 ViewModel:
implementation “android.arch.lifecycle:livedata:1.1.1”
implementation “android.arch.lifecycle:viewmodel:1.1.1”
接下来就对照上面讲的三步走战略,创建如下代码:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private static final String TAG = “MainActivity”;

private MutableLiveData<Integer> mNumberLiveData;
private TextView mTvNumber;
private Button mBtnStart;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvNumber = findViewById(R.id.tv_number);
mBtnStart = findViewById(R.id.btn_start);
mBtnStart.setOnClickListener(this);

mNumberLiveData = new MutableLiveData<>();

mNumberLiveData.observe(this, new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
mTvNumber.setText(“” + integer);
Log.d(TAG, “onChanged: ” + integer);
}
});
}

@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
super.run();
int number = 0;
while (number < 5) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
number++;
mNumberLiveData.postValue(number);
}
}
}.start();
}
}
这里,我们在 onCreate 方法中创建了一个 MutableLiveData 类型的变量 mNumberLiveData,并将其泛型指定为 Integer,通过其 observe(…)方法把 this 传进去(this 为 AppCompatActivity, 实现了 LifecycleOwner 接口,支持包为 28.0.0),并传进去一个 Observer, 在其 onChanged(…)方法中,我们将变化后的数据 integer 设置给 TextView 显示。为了便于观察,我们同时在控制台打印一行对应的日志。

Demo 的界面很简单,就是一个按钮,一个 TextView , 点击按钮,开启一个子线程,每过 3 秒通过 postValue(…)修改 LiveData 中的值 (如果是在 UI 线程,可以直接通过 setValue(…) 来修改)。
这里我们点击开始,并在数字还没变为 5 的时候,就按 Home 键进入后台,等过一段时间之后,在进入页面,会发现页面最终显示为数字“5”,但是打印的结果并不是连续的 1~5, 而是有中断:

这也证明了当程序进入后台,变为 inactive 状态时,并不会收到数据更新的通知,而是在重新变为 active 状态的时候才会收到通知,并执行 onChanged(…)方法。
上面可以看到,我们使用 LiveData 的时候,实际使用的是它的子类 MutableLiveData,LiveData 是一个接口,它并没有给我们暴露出来方法供我们对数据进行修改。如果我们需要对数据修改的时候,需要使用它的具体实现类 MutableLiveData,其实该类也只是简单的将 LiveData 的 postValue(…)和 setValue(…)暴露了出来:
public class MutableLiveData<T> extends LiveData<T> {
@Override
public void postValue(T value) {
super.postValue(value);
}

@Override
public void setValue(T value) {
super.setValue(value);
}
}
MutableLiveData<T> 其实是对数据进行了一层包裹。在它的泛型中可以指定我们的数据类。可以存储任何数据,包括实现了 Collections 接口的类,比如 List。
扩展 LiveData
有时候我们需要在 observer 的 lifecycle 处于 active 状态时做一些操作,那么我们就可以通过继承 LiveData 或者 MutableLiveData,然后覆写其 onActive()和 onInactive()方法。这两个方法的默认实现均为空。像下面这样:
public class StockLiveData extends LiveData<BigDecimal> {
private StockManager stockManager;

private SimplePriceListener listener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};

public StockLiveData(String symbol) {
stockManager = new StockManager(symbol);
}

@Override
protected void onActive() {
stockManager.requestPriceUpdates(listener);
}

@Override
protected void onInactive() {
stockManager.removeUpdates(listener);
}
}

LiveData 具有生命周期感知能力,能在 Activity 销毁的时候自动取消监听,这也意味着它可以用来在多个 Activity 间共享数据。我们可以借助单例来实现,这里直接饮用官方 Demo:
public class StockLiveData extends LiveData<BigDecimal> {
private static StockLiveData sInstance;
private StockManager stockManager;

private SimplePriceListener listener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};

@MainThread
public static StockLiveData get(String symbol) {
if (sInstance == null) {
sInstance = new StockLiveData(symbol);
}
return sInstance;
}

private StockLiveData(String symbol) {
stockManager = new StockManager(symbol);
}

@Override
protected void onActive() {
stockManager.requestPriceUpdates(listener);
}

@Override
protected void onInactive() {
stockManager.removeUpdates(listener);
}
}
转换 LiveData
有时候,我们需要在将 LiveData 中存储的数据分发给 Observer 之前进行一些修改。比如我们例子中拿到的是 Integer 类型的返回值,我们设置进 TextView 的时候,直接使用 mTvNumber.setText(integer)会报错,需要使用 mTvNumber.setText(“” + integer)这种形式,但我想在这里直接拿到已经处理过的 String 数据,拿到就能直接用,而不需要再在这里手动拼。我们可以通过 Transformations 类的 map 操作符来实现这个功能。
原始的代码为:
mNumberLiveData = new MutableLiveData<>();

mNumberLiveData.observe(this, new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
mTvNumber.setText(“” + integer);
Log.d(TAG, “onChanged: ” + integer);
}
});
使用 Transformations.map(…)改造之后的代码:
mNumberLiveData = new MutableLiveData<Integer>();

Transformations.map(mNumberLiveData, new Function<Integer, String>() {
@Override
public String apply(Integer integer) {
return “” + integer;
}
}).observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
mTvNumber.setText(s);
Log.d(TAG, “onChanged: ” + s);
}
});
这就实现了将一种类型的数据转化为另一种类型的数据。map 操作符会返回一个改造之后的 LiveData,直接对这个 LiveData 进行监听即可。这里的 map 操作符类似于 RxJava 的 map。
但有时候我们并不只是需要简单的把数据由一种类型转为另一种类型。我们可能需要的更高级一点。
比如,我们一方面需要一个存储 userId 的 LiveData,另一方面又需要维护一个存储 User 信息的 LiveData,而后者的 User 则是根据 userId 来从数据库中查找的,二者需要对应。这时候我们就可以使用 Transformations 类的 switchMap(…)操作符。
MutableLiveData<String> userIdLiveData = new MutableLiveData<>();

LiveData<User> userLiveData = Transformations.switchMap(userIdLiveData, new Function<String, LiveData<User>>() {
@Override
public LiveData<User> apply(String userId) {
// 根据 userId 返回一个 LiveData<User>,可以通过 Room 来获取
return getUser(userId);
}
});
这里,我们在覆写的 apply(…)方法中,每次 userId 发生变化之后,会自动通过 getUser(userId) 去获取一个封装有 User 对象的 LiveData。如果是从数据库获取的话,使用 Google 推出的配套的数据库组件 Room 会比较爽,因为它能直接返回一个 LiveData。关于 Room,有时间的话之后再写文章讲解。
从上面可以看出,LiveData 包中提供的 Transformations 非常有用,能让我们的整个调用过程变成链式。但 Transformations 只提供了 map(…)和 switchMap(…)两个方法,如果我们有其他更复杂的需求,就需要自己通过 MediatorLiveData 类来创建自己的 transformations。话说回来,其实上面两个方法的内部,就是通过 MediatorLiveData 来实现的,通过 MediatorLiveData 进行了一次转发。这里贴出 Transformations 的源码:
public class Transformations {

private Transformations() {
}

@MainThread
public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
@NonNull final Function<X, Y> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
@Override
public void onChanged(@Nullable X x) {
result.setValue(func.apply(x));
}
});
return result;
}

@MainThread
public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
@NonNull final Function<X, LiveData<Y>> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(trigger, new Observer<X>() {
LiveData<Y> mSource;

@Override
public void onChanged(@Nullable X x) {
LiveData<Y> newLiveData = func.apply(x);
if (mSource == newLiveData) {
return;
}
if (mSource != null) {
result.removeSource(mSource);
}
mSource = newLiveData;
if (mSource != null) {
result.addSource(mSource, new Observer<Y>() {
@Override
public void onChanged(@Nullable Y y) {
result.setValue(y);
}
});
}
}
});
return result;
}
}
源码比较简单,不再详细讲解。
它里面其实主要用的就是 MediatorLiveData,通过该类我们能组合多个 LiveData 源。当任何一个 LiveData 源发生改变的时候,MediatorLiveData 的 Observers 都会被触发,这点比较实用。比如我们有两个 LiveData, 一个是从数据库获取,一个是从网络获取。通过 MediatorLiveData 就能做到,当二者任何一个获取到最新数据,就去触发我们的监听。
顺便也贴下 MediatorLiveData 的源码,它继承自 MutableLiveData:
public class MediatorLiveData<T> extends MutableLiveData<T> {
private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();

@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
“This source was already added with the different observer”);
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}

@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote) {
Source<?> source = mSources.remove(toRemote);
if (source != null) {
source.unplug();
}
}

@CallSuper
@Override
protected void onActive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().plug();
}
}

@CallSuper
@Override
protected void onInactive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().unplug();
}
}

private static class Source<V> implements Observer<V> {
final LiveData<V> mLiveData;
final Observer<V> mObserver;
int mVersion = START_VERSION;

Source(LiveData<V> liveData, final Observer<V> observer) {
mLiveData = liveData;
mObserver = observer;
}

void plug() {
mLiveData.observeForever(this);
}

void unplug() {
mLiveData.removeObserver(this);
}

@Override
public void onChanged(@Nullable V v) {
if (mVersion != mLiveData.getVersion()) {
mVersion = mLiveData.getVersion();
mObserver.onChanged(v);
}
}
}
}
这里顺便提一句,如果想在数据更新的时候让 Observer 立即得到通知,也就是说忽略生命周期状态,这时候我们可以使用 LiveData 的 observeForever(Observer<T> observer)方法。
LiveData 往往是需要结合 ViewModel 才能发挥出更大的威力。下面就接着介绍 ViewModel 的知识,以及二者的搭配使用。
什么是 ViewModel
简单来讲,ViewModel 是一种用来存储和管理 UI 相关数据的类。但不同的是,它支持在系统配置发生改变的时候自动对数据进行保存。当然,这要配合 LiveData。
我们知道,在屏幕旋转的时候,会导致 Activity/Fragment 重绘,会导致我们之前的数据丢失。就比如,如果我们使用 EditText, 在里面输入了内容,但是屏幕旋转的时候,会发现其中的 text 内容被清空了。如果你发现没清空,可能使用的是 support 包下的控件,或者 Activity 继承自 AppCompatActivity,并且给该控件添加了 id。系统对一些简单的数据进行了恢复(其实是在 EditText 的父类 TextView 进行的恢复)。
对于一些简单的数据,我们可以通过在 Activity 的 onSaveInstanceState()方法中存储,然后在 onCreate()中进行恢复,但是这种方式只适合存储少量的数据,并且是能被序列化和反序列化的数据。而对那些大量的数据则不适用,比如一个 User 或者 Bitmap 的 List。
此外,它也使得 View 的数据持有者和 UI controller 逻辑更加分离,便于解耦和测试。
LiveData 结合 ViewModel 使用
之前我们是单独使用 LiveData,这里配合 ViewModel 使用:
public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<List<User>>();
loadUsers();
}
return users;
}

private void loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
可以看到,这里我们创建一个类,继承自 ViewModel,然后在里面存储我们需要的 MutableLiveData 字段。注意,getUsers()方法返回的类型是 LiveData 而非 MutableLiveData,因为我们一般不希望在 ViewModel 外面对数据进行修改,所以返回的是一个不可变的 LiveData 引用。如果想对数据进行更改,我们可以暴露出来一个 setter 方法。
接下来可以按照如下的方式获取 ViewModel:
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
// Create a ViewModel the first time the system calls an activity’s onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.

MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {
// update UI
});
}
}
我们在 onCreate()方法中通过 ViewModelProviders.of(this).get(MyViewModel.class); 这行代码来获取一个 MyViewModel 实例。之后又通过该实例暴露出来的 getter 方法获取 LiveData 实例。这里要注意,当 Activity 重建的时候,虽然 onCreate() 方法会重新走一遍,但是这个 MyViewModel 实例,仍然是第一次创建的那个实例,在 ViewModelProviders.of(this).get(***.class)中的 get 方法中进行了缓存。之后进行源码解析的时候会详细讲解。先看下下面的一张图,了解下 ViewModel 的整个生命周期:

ViewModel 最终消亡是在 Activity 被销毁的时候,会执行它的 onCleared()进行数据的清理。
Fragment 间进行数据共享
Fragment 间共享数据比较常见。一种典型的例子是屏幕左侧是一个 Fragment,其中存储了一个新闻标题列表,我们点击一个 item,在右侧的 Fragment 中显示该新闻的详细内容。这种场景在美团等订餐软件中也很常见。
通过 ViewModel 将使得数据在各 Fragment 之间的共享变得更加简单。
我们需要做的仅仅是在各 Fragment 的 onCreate() 方法中通过:
ViewModelProviders.of(getActivity()).get(***ViewModel.class);
来获取 ViewModel , 注意,of(…)方法中传入的是二者所在的 activity。具体可以参考如下官方代码:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

public void select(Item item) {
selected.setValue(item);
}

public LiveData<Item> getSelected() {
return selected;
}
}

public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 传入 activity
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}

public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 传入 activity
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// Update the UI.
});
}
}
Android 3.0 中引入了 Loader 机制,让开发者能轻松在 Activity 和 Fragment 中异步加载数据。但事实上用的人并不多。现在,它几乎可以退出历史舞台了。ViewModel 配合 Room 数据库以及 LiveData, 完全可以替代 Loader,在 SDK28 里,也越来越多的用 Loader 也越来越多的被替代。
但要注意,ViewModel 能用来替换 Loader,但是它却并不是设计用来替换 onSaveInstanceState(…)的。关于数据持久化以及恢复 UI 状态等,可以参考下 Medium 上的这篇文章,讲的简直不能再好了:ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders
总结
通常 LiveData 是需要配合 ViewModel 使用的。ViewModel 负责在系统配置更改时保存和恢复 LiveData,而 LiveData 则负责在生命周期状态发生改变的时候,对数据的变化进行监听。
写到这里算是把 LiveData 和 ViewModel 的使用讲完了。这里我在开篇故意单独把 LiveData 和 ViewModel 分开讲解,相比较官网更加容易理解。但如果想对二者进行详细了解,还是建议把官方文档认真的多阅读几遍。
欢迎关注公众号来获取最新消息。

正文完
 0