【Android】LiveData

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

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

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内部
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能感知生命周期的关键。

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>可读可写,继承自LiveDataViewModel内部使用,负责生产和修改数据

最佳实践: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包装类 + 单次消费标记

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中使用:

private final MutableLiveData<Event<String>> _toast = new MutableLiveData<>();
public LiveData<Event<String>> getToast() {
    return _toast;
}

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

Activity中观察:

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”的字符串再显示:

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查询用户信息:

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异步的,要去拿新数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值