【Android】LiveData

一、为什么需要LiveData:只用ViewModel的局限

在上一篇的计数器写法中,数据保存在了ViewModel里,但更新UI仍然是手动调用setText:

java 复制代码
btnAdd.setOnClickListener(v -> {
    viewModel.add();
    // 手动刷新UI
    tvCount.setText(String.valueOf(viewModel.count));
});

这种写法存在很多潜在的问题:

  • 无法自动感知页面生命周期。如果网络请求/异步任务在ViewModel里执行,回调时Activity已经退到后台或已经销毁,此时强行更新UI会导致空指针 、内存泄漏。
  • 数据驱动能力弱。数据和UI强耦合,不符合MVVM的"数据驱动视图"思想;数据变化无法自动分发到多个观察者。

LiveData的核心定位 :它是一个可观察的数据持有类 ,并且具备生命周期感知能力。数据变化时,自动通知处于活跃状态的页面更新UI,页面销毁时自动解绑,彻底解决上述问题。


二、LiveData核心特性

  1. 可观察:数据发生变化时,自动通知所有观察者(Activity/Fragment)
  2. 生命周期感知:只给处于**活跃状态(STARTED/RESUMED)**的页面分发数据,页面销毁自动解除订阅,不会内存泄漏
  3. 数据自动恢复:配置变更(旋转屏幕)导致页面重建,会自动收到最新的一次数据,不用手动保存恢复
  4. UI与数据解耦:ViewModel只负责生产数据,Activity只负责观察并渲染,互不持有
  5. 线程安全友好:区分主线程/子线程更新数据的API

三、基础使用:改造计数器案例

我们把上一篇文章的计数器,改成ViewModel + LiveData的标准写法。

1. 在ViewModel中定义LiveData

核心规则:

  • 对外暴露只读的LiveData,防止外部随意修改数据
  • 内部使用可写的MutableLiveData,只允许ViewModel内部修改数据
  • 数据的所有变更逻辑,都封装在ViewModel内部
java 复制代码
public class CounterViewModel extends ViewModel {
    
    //内部可写的MutableLiveData,private修饰,只在ViewModel内部修改
    private final MutableLiveData<Integer> _count = new MutableLiveData<>(0);
    
    //对外暴露只读的LiveData,供Activity观察
    public LiveData<Integer> getCount() {
        return _count;
    }
    
    //加法业务方法
    public void add() {
        //获取当前值
        Integer current = _count.getValue();
        if(current == null) {
            current = 0;
        }
        _count.setValue(current + 1);
    }
    
    //重置方法
    public void reset() {
        _count.setValue(0);
    }
}

2. 在Activity中观察LiveData

使用observe() 方法订阅数据变化,传入LifecycleOwner(也就是Activity自身),这是LiveData能感知生命周期的关键。

java 复制代码
public class MainActivity extends AppCompatActivity {

    private TextView tvCount;
    private CounterViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        tvCount = findViewById(R.id.tv_count);
        Button btnAdd = findViewById(R.id.btn_add);
        Button btnReset = findViewById(R.id.btn_reset);

        //获取ViewModel实例
        //通过ViewModelProvider获取实例,绑定当前Activity
        viewModel = new ViewModelProvider(this).get(CounterViewModel.class);

        //观察count数据的变化
        viewModel.getCount().observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer integer) {
                //数据变化时,自动回调这里,直接更新UI
                tvCount.setText(String.valueOf(integer));
            }
        });

        //点击事件只触发业务逻辑,不手动刷新UI
        btnAdd.setOnClickListener(v -> viewModel.add());
        btnReset.setOnClickListener(v -> viewModel.reset());
    }
}

3、使用效果

  • 点击按钮→ViewModel里的_count数据变化→LiveData自动触发onChanged→UI自动更新
  • 旋转屏幕→Activity重建→重新observe→立刻收到最新的count值→UI自动恢复,无需手动保存
  • 按Home键回到后台→LiveData停止分发数据,避免后台更新UI导致异常
  • 退出界面→LiveData自动解绑,不会内存泄漏

四、核心API详解

1. LiveData与MutableLiveData的区别

|----------------------|------------------|-------------------------|
| 类 | 可读性 | 使用场景 |
| LiveData<T> | 只读,只能观察,不能外部修改 | 对外暴露,供View层订阅,保证数据流向唯一 |
| MutableLiveData<T> | 可读可写,继承自LiveData | ViewModel内部使用,负责生产和修改数据 |

最佳实践:ViewModel永远对外暴露LiveData,内部用MutableLiveData,遵循数据单向流动原则:View→ViewModel触发事件→ViewModel修改LiveData→LiveData通知View更新。

2. setValue()与postValue()的区别

  • setValue(T value)
    • 必须在主线程调用
    • 立刻更新值,立刻通知活跃的观察者
    • 多次调用,每次都会触发回调
  • postValue(T value)
    • 可以在子线程调用,内部会自动切到主线程
    • 不是立即执行,而是在post到主线程消息队列
    • 短时间内多次调用,只有最后一次的值会被分发,中间值会被丢弃

使用规则:

  • 主线程修改数据→用setValue
  • 子线程(网络请求回调、IO线程)修改数据→用postValue
  • 高频更新场景慎用postValue,可能丢中间值

3. observe()与observeForever()

  • observe(LifecycleOwner owner, Observer observer)
    • 绑定生命周期,页面活跃才回调,销毁自动解绑
  • observeForever(Observer observer)
    • 不绑定生命周期,只要数据变就一直回调
    • 必须手动调用removeObserver()解绑,否则会内存泄漏
    • 非特殊场景严禁使用

五、生命周期感知的底层原理

LiveData通过Lifecycle框架,监听Activity/Fragment的生命周期状态,只在活跃态分发数据。销毁态自动移除观察者。

简单流程:

  1. 调用observe(this, observer)时,LiveData会把观察者和LifecycleOwner绑定
  2. 当页面生命周期变化(onStart、onResume、onPause、onDestroy),LiveData都能收到通知
  3. 状态变为START或RESUMED(活跃态):视为可接收数据,有新数据就回调
  4. 状态变为DESTROYED(页面销毁):自动移除观察者,不会泄露,不会空指针
  5. 页面从后台回到前台:如果期间有数据自动更新,回到前台时会自动收到最新值

六、粘性事件(数据倒灌)与解决方案

1. 什么是粘性事件

LiveData有一个默认特性:新观察者订阅时,会立刻收到LiveData里当前存储的最后一次数据。

  • 优点:旋转屏幕重建页面,能立刻恢复最新数据
  • 缺点:对于"一次性事件"(比如Toast、弹窗、页面跳转)时,会出现"倒灌"问题

举一个聊天室的简单例子:

  • 聊天页ViewModel里有一个toastMsg的LiveData,用来作为消息是否发送成功的标志位,不成功则弹提示
  • 发送消息失败,setValue("发送失败"),页面谈了一次Toast
  • 旋转屏幕,页面重建,重新observe,又收到"发送失败",再弹一次Toast,甚至切后台回来,也可能重复弹

这就是粘性事件/数据倒灌,是LiveData最著名的坑。

2. 解决方案

日常开发最常用的方法是:Event包装类 + 单次消费标记

java 复制代码
public class Event<T> {
    private final T content;
    private boolean hasBeenHandled = false;
    
    public Event(T content) {
        this.content = content;
    }
    
    //获取内容,如果未被消费则返回,否则返回null
    public T getContentIfNotHandled() {
        if(hasBeenHandled) {
            return null;
        }
        hasBeenHandled = true;
        return content;
    }
    //强制获取当前内容,无论是否消费过
    public T peekCotent() {
        return content;
    }
}

ViewModel中使用:

java 复制代码
private final MutableLiveData<Event<String>> _toast = new MutableLiveData<>();
public LiveData<Event<String>> getToast() {
    return _toast;
}

public void sendMessage() {
    // 发送失败,触发一次性提示
    _toast.setValue(new Event<>("发送失败"));
}

Activity中观察:

java 复制代码
viewModel.getToast().observe(this, event -> {
    String msg = event.getContentIfNotHandled();
    if (msg != null) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
});

这样同一条消息只会被消费一次,旋转屏幕也不会重复弹出。


七、数据转换:Transformations

当需要对LiveData的数据做转换后再交给UI,可以用Transformations.mapTransformations.switchMap,无需自己写中间层。

1. map:一对一数据转换

比如计数器数字,要转成"当前计数:X"的字符串再显示:

java 复制代码
public class CounterViewModel extends ViewModel {
    private final MutableLiveData<Integer> _count = new MutableLiveData<>(0);

    // 将Int类型的count,转换为String类型的展示文本
    public final LiveData<String> countText = Transformations.map(_count, count -> {
        return "当前计数:" + count;
    });

    public void add() {
        Integer current = _count.getValue();
        if (current == null) current = 0;
        _count.setValue(current + 1);
    }
}

UI层直接观察countText即可,不用自己做字符串拼接。

2. switchMap:触发式切换数据源

常用于"根据一个参数,切换不同的数据源",比如根据用户ID查询用户信息:

java 复制代码
private final MutableLiveData<String> userId = new MutableLiveData<>();

// userId变化时,自动切换到对应数据源
public LiveData<User> user = Transformations.switchMap(userId, id -> {
    return repository.getUserById(id); // 返回一个LiveData
});

public void setUserId(String id) {
    userId.setValue(id);
}

他跟map的区别在哪呢?

  • map"加工" 。把 A 变成 B(比如把 Int 变成 String)。同步的,瞬间完成。

  • switchMap"换源" 。因为 A 变了,所以把整个数据源换成另一个 LiveData异步的,要去拿新数据。

相关推荐
格林威2 小时前
C#图像处理:使用imagemagick实现像素放大水印转换等多种功能
android·开发语言·图像处理·人工智能·计算机视觉·c#·视觉检测
雨白9 小时前
深入理解 Java/Kotlin 反射、注解与泛型:从原理到实战
android·架构
喜欢打篮球的普通人13 小时前
LLVM Backend Lowering 从入门到实战:把 IR 变成机器码的完整链路
android·java·数据库
萝卜er16 小时前
BroadcastReceiver 静态注册与动态注册-《Android深水区(七)》
android
萝卜er16 小时前
ContentProvider 与跨进程数据访问-《Android深水区(八)》
android
萝卜er16 小时前
Service、前台服务与后台限制-《Android深水区(六)》
android
萝卜er16 小时前
Android 存储体系与分区存储-《Android深水区(十二)》
android
会编程的吕洞宾17 小时前
DeepAgents In Action学习(Second)
android·java·学习
太子釢17 小时前
用 AI 完整实现 Github 客户端:基于 KMP + SwiftUI + Compose
android·人工智能·ios