Android MVVM 架构实战:深入理解 LiveData 生命周期与数据观察
详细讲解了 Android 架构组件 LiveData 的核心原理与实战应用。内容涵盖 LiveData 的生命周期感知特性、内存泄漏防护机制、与 ViewModel 的配合方式、Room 数据库集成方案以及 Transformations 转换工具的高级用法。文章修正了常见代码错误,补充了 SingleLiveEvent 最佳实践及线程安全注意事项,旨在帮助开发者构建稳定高效的 MVVM 架构应用。

详细讲解了 Android 架构组件 LiveData 的核心原理与实战应用。内容涵盖 LiveData 的生命周期感知特性、内存泄漏防护机制、与 ViewModel 的配合方式、Room 数据库集成方案以及 Transformations 转换工具的高级用法。文章修正了常见代码错误,补充了 SingleLiveEvent 最佳实践及线程安全注意事项,旨在帮助开发者构建稳定高效的 MVVM 架构应用。

LiveData 是 Google 推荐的 Android 架构组件之一,属于 Jetpack 库的一部分。它是一个存放可被观察的数据持有类,具备生命周期感知功能,有效解决了 Android 开发者在处理 UI 更新时需要手动管理生命周期的痛点。
例如,在使用 Retrofit+RxJava 处理接口回调数据时,开发者必须考虑 Activity 或 Fragment 的生命周期,以防止在 onStop 或 onDestroy 之后收到回调导致崩溃。现在只需使用 Retrofit+LiveData,其余的生命周期处理工作由 LiveData 自动完成。
LiveData 遵循观察者模式,并会在生命周期状态变化时通知观察者。它优雅地处理了生命周期问题,并非所有数据变化都会触发回调,只有在观察者处于活跃状态(Active)时才会通知,因此可以在回调中安全地执行 UI 更新操作。
观察者都是绑定 Lifecycle 的。当 LifecycleOwner(如 Activity)销毁时,LiveData 会自动移除观察者,从而防止因强引用导致的内存泄漏。
LiveData 将 Activity 生命周期合并分为 inactive 与 active 两种状态(start 与 resume 为 active,其他为 inactive)。处于非活跃(inactive)状态时,不会收到 LiveData 的任何事件通知,避免了在非可见状态下更新 UI 导致的异常。
UI 组件只需要关心相关的数据,不需要去手动变换生命周期状态,LiveData 已经帮你搞定了。这大大降低了代码复杂度。
简单来说,LiveData 在生命周期非活状态(inactive)期间,如果数据发生变化,当状态变为活状态(active)的时候,会回调一次最终的数据。这确保了用户重新进入页面时能看到最新状态。
像屏幕旋转导致的 Activity 或 Fragment 重创建之后,LiveData 会立即通知一下相应的观察者。由于 LiveData 存储在 ViewModel 中,配置更改后数据不会丢失,保证了用户体验的连续性。
您可以使用单例模式扩展 LiveData 对象并包装成系统服务,以便在应用程序中进行共享。LiveData 对象一旦连接到系统服务,任何需要该资源的 Observer 都只需观察这个 LiveData 对象,实现了数据的集中管理。
注意:可以使用 observeForever(Observer) 方法注册一个没有关联 LifecycleOwner 对象的 Observer。在这种情况下,Observer 被认为始终处于活动状态,因此当有数据变化时总是会被通知。您可以调用 removeObserver(Observer) 方法移除这些 Observer。
当你更新 LiveData 对象中存储的数据时,所有注册了的 Observer,只要所绑定的 LifecycleOwner 处于活动状态,就会被触发通知。LiveData 允许 UI 控制器 Observer 订阅更新。当 LiveData 对象所保存的数据发生变化时,UI 会在响应中自动更新。
LiveData 是一个包装器,可用于任何数据,包括实现 Collections 的对象,如 List。一个 LiveData 对象通常存储在 ViewModel 对象中,并通过 getter 方法访问。
public class NameViewModel extends ViewModel {
// Create a LiveData with a String
private MutableLiveData<String> mCurrentName;
public MutableLiveData<String> getCurrentName() {
if (mCurrentName == null) {
mCurrentName = new MutableLiveData<>();
}
return mCurrentName;
}
}
起初,LiveData 对象中的数据未设置。确保在 ViewModel 而不是 Activity 或 Fragment 中保存用来更新 UI 的 LiveData 对象,原因如下:
在大多数情况下,出于以下原因,应用程序组件的 onCreate() 方法是开始观察 LiveData 对象的最佳位置:
通常情况下,LiveData 只在数据有变化时,给活跃的 Observer 进行通知。此行为的一个例外是,Observer 在从非活跃状态变为活跃状态时也会收到通知。并且,如果 Observer 第二次从非活跃状态变为活跃状态,则只有在自上一次变为活跃状态以来该数据发生变化时才会接收到更新。
以下示例代码演示了如何开始观察 LiveData 对象:
public class NameActivity extends AppCompatActivity {
private NameViewModel mModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Other code to setup the activity...
// Get the ViewModel.
mModel = ViewModelProviders.of(this).get(NameViewModel.class);
// Create the observer which updates the UI.
final Observer<String> nameObserver = new Observer<String>() {
@Override
public void onChanged(@Nullable final String newName) {
// Update the UI, in this case, a TextView.
mNameTextView.setText(newName);
}
};
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
mModel.getCurrentName().observe(this, nameObserver);
}
}
传递 nameObserver 作为参数,调用 observe() 之后,将立即回调 onChanged(),提供存储在 mCurrentName 中的最新值。如果 LiveData 对象 mCurrentName 的值并未设置,则不调用 onChanged()。
LiveData 没有公用的方法来更新存储的数据。MutableLiveData 类暴露公用的 setValue(T) 和 postValue(T) 方法,如果需要编辑存储在 LiveData 对象中的值,必须使用这两个方法。通常在 ViewModel 中使用 MutableLiveData,然后 ViewModel 仅向 Observer 公开不可变的 LiveData 对象。
在建立观察者关系之后,可以更新 LiveData 对象的值,如以下示例所示,当用户点击按钮时向所有观察者发出通知:
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String anotherName = "John Doe";
mModel.getCurrentName().setValue(anotherName);
}
});
在示例中调用 setValue(T) 会导致 Observer 使用值 "John Doe" 调用 onChanged() 方法。这个例子展示了点击按钮,setValue() 或者 postValue() 被调用来更新 mName,原因有多种,包括响应网络请求或加载完成;在所有情况下,调用 setValue() 或 postValue() 都会触发观察者并更新 UI。
注意:必须要从主线程调用 setValue(T) 方法来更新 LiveData 对象。如果代码在工作线程中执行,你可以使用 postValue(T) 方法来更新 LiveData 对象。
Room 持久性库支持 Observable 查询返回 LiveData 对象。Observable 查询成为数据库访问对象(DAO)的一项功能。当更新数据库时,会生成所有必要的代码来更新 LiveData 对象。生成的代码在需要时在后台线程上异步运行查询。这种模式对于保持用户界面中显示的数据与存储在数据库中的数据同步很有用。
以下是一个完整的 Room DAO 示例:
@Dao
public interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
LiveData<User> getUserById(int id);
@Insert
void insert(User user);
}
在 ViewModel 中直接暴露 DAO 的查询结果:
public class UserViewModel extends ViewModel {
private UserDao userDao;
public LiveData<User> getUser(int id) {
return userDao.getUserById(id);
}
}
如果 Observer 的生命周期处于 STARTED 或 RESUMED 状态,则 LiveData 将认为 Observer 处于活动状态。以下示例代码说明了如何扩展 LiveData 类:
public class StockLiveData extends LiveData<BigDecimal> {
private StockManager mStockManager;
private SimplePriceListener mListener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};
public StockLiveData(String symbol) {
mStockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
mStockManager.requestPriceUpdates(mListener);
}
@Override
protected void onInactive() {
mStockManager.removeUpdates(mListener);
}
}
本例中 StockLiveData 的实现包括以下重要的方法:当 LiveData 对象有一个活跃的 Observer 时,onActive() 方法被调用。这意味着你需要从这个方法开始观察股票价格的更新。当 LiveData 对象没有任何活跃的 Observer 时,onInactive() 方法被调用。由于没有 Observer 在监听,所以没有理由继续保持与 StockManager 服务的连接。setValue(T) 方法更新 LiveData 实例的值,并通知活动观察者有关更改。
为了保持简洁,你可以使用单例模式实现 LiveData:
public class StockLiveData extends LiveData<BigDecimal> {
private static StockLiveData sInstance;
private StockManager mStockManager;
private SimplePriceListener mListener = 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) {
mStockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
mStockManager.requestPriceUpdates(mListener);
}
@Override
protected void onInactive() {
mStockManager.removeUpdates(mListener);
}
}
多个 Activity 和 Fragment 可以观察到 MyPriceListener 实例。LiveData 只在他们至少一个处于可见和活跃状态时才连接到系统服务。
您可能希望先转换存储在 LiveData 对象中的值,然后再将其分派给 Observer,或者您可能需要根据一个 LiveData 实例的值返回不同的 LiveData 实例。Lifecycle 包提供了 Transformations 类,提供了支持这些使用场景的方法。
Transformations.map() 使用一个函数来转换存储在 LiveData 对象中的值,并向下传递转换后的值。
LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
return user.name + " " + user.lastName;
});
Transformations.switchMap() 与 map() 类似,使用一个函数来转换存储在 LiveData 对象中的值,并向下传递结果。传递给 switchMap() 的函数必须返回一个 LiveData 对象。
private LiveData<User> getUser(String id) {
...
}
LiveData<String> userId = ...;
LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id));
您可以使用转换方法在 Observer 的生命周期中传递信息。除非 Observer 正在观看返回的 LiveData 对象,否则不会计算转换。由于转换是延迟计算的,所以与生命周期相关的行为隐式传递,而不需要额外的显式调用或依赖关系。
如果您认为在 ViewModel 对象中需要 Lifecycle 对象,则转换可能是更好的解决方案。例如,假设您有一个接受地址并返回该地址的邮政编码的 UI 组件。您可以为此组件实现朴素的 ViewModel:
class MyViewModel extends ViewModel {
private final PostalCodeRepository repository;
private final MutableLiveData<String> addressInput = new MutableLiveData();
public final LiveData<String> postalCode =
Transformations.switchMap(addressInput, (address) -> {
return repository.getPostCode(address);
});
public MyViewModel(PostalCodeRepository repository) {
this.repository = repository;
}
private void setInput(String address) {
addressInput.setValue(address);
}
}
在这种情况下,postalCode 字段是公开的和最终的,因为该字段永远不会改变。postalCode 字段定义为 addressInput 的转换,这意味着 addressInput 发生更改时,如果有一个活跃的 Observer,将调用 repository.getPostCode() 方法。如果在 repository.getPostCode() 被调用时没有活跃的 Observer,直到添加一个观察者才会进行计算。此机制允许较低级别的应用程序创建按需延迟计算的 LiveData 对象。
虽然 LiveData 很好,但在某些场景下(如导航、Toast 提示),我们希望事件只触发一次。普通的 LiveData 在配置更改后会再次触发 onChanged。此时可以使用 SingleLiveEvent 模式,通过 AtomicInteger 标记是否已发送过事件,确保一次事件只被消费一次。
在使用 LiveData 获取数据时,务必注意初始值为 null 的情况。建议在 onChanged 中对数据进行非空判断,或者使用 Optional 等工具类辅助。
setValue 必须在主线程调用,postValue 可以在子线程调用。如果在子线程中直接调用 setValue,应用可能会崩溃。务必使用 Handler 或 postValue 确保线程安全。
LiveData 是构建现代化 Android 应用不可或缺的工具。它通过生命周期感知能力,极大地简化了数据流管理,减少了内存泄漏风险,并提升了代码的可维护性。结合 ViewModel 和 Room,开发者可以轻松构建出健壮、高效的 MVVM 架构应用。在实际开发中,合理运用 Transformations 和自定义 LiveData 子类,能够进一步发挥其灵活性,满足复杂业务场景的需求。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online