学习记录 (5)- LiveData

前言

学习记录系列是通过阅读学习《Android Jetpack应用指南》对书中内容学习记录的Blog,《Android Jetpack应用指南》京东天猫有售,本文是学习记录的第五篇。


简介

LiveData(实时数据) 是一个可被观察的数据容器类。具体说来,可以将 LiveData 理解为一个数据的容器,它将数据包装起来,使数据包装起来,使数据成为被观察者,当该数据发生变化时,观察者能够获得通知。我们不需要自己去实现观察者模式, LiveData 内部已经默认实现,只要使用就可以了。

LiveData 和 ViewModel 的关系

1、ViewModel 用于存放页面所需要的各种数据,不仅如此,还可以在其中放一些与数据相关的业务逻辑。例如,可以在 ViewModel 中进行数据的加工,获取等操作。因此,ViewModel 中的数据可能会随着业务的变化而变化。
2、对页面来说,它并不关心 ViewModel 中的业务逻辑,它只关心需要展示的数据是什么,并且希望在数据发生变化时,能及时得到通知并做出更新。LiveData 的作用就是,在 ViewModel 中的数据发生变化时通知页面。因此,LiveData 通常被放在 ViewModel 中使用, 用于包装 ViewModel 中那些需要被外界观察的数据。


LiveData 的基本使用方法

在第 4 章 ViewMoedl 的计时器案例的基础上,使用 LiveData 对接口进行改写
1.LiveData 是一个抽象类,不能直接使用。通常使用的是它的直接直接子类 MutableLiveData

/**
 * @author JinXin 2020/11/3
 */
public class TimerViewModel extends ViewModel {

    private static final String TAG = "TimerViewModel";

    private Timer timer;
    private int currentSecond;

    /**
     * 将 currentSecond 这个字段用 MutableLiveData 包装起来
     */
    private MutableLiveData<Integer> liveCurrentSecond;

    public MutableLiveData<Integer> getLiveCurrentSecond() {
        if (liveCurrentSecond == null) {
            liveCurrentSecond = new MutableLiveData<>();
        }
        return liveCurrentSecond;
    }

    /**
     * ViewModel最重要的作用是将视图与数据分离,并独立于Activity的重建。
     * 为了验证这样一点,在ViewModel中创建一个计时器Timer,每隔1s通过 liveData 通知观察者更UI
     */
    public void startTiming() {

        if (timer == null) {

            currentSecond = 0;

            timer = new Timer();
            TimerTask timerTask = new TimerTask() {
                @Override
                public void run() {
                    currentSecond ++;
                    getLiveCurrentSecond().postValue(currentSecond);
                }
            };
            timer.schedule(timerTask, 1000, 1000);
        }
    }


    /**
     * ViewModel是一个抽象类,其中只有一个onCleared()方法。
     * 当ViewModel不再被需要,即与之相关的Activity都被销毁时,
     * 该方法会被系统调用。可以在该方法中执行一些资源释放相关操作
     */
    @Override
    protected void onCleared() {
        super.onCleared();
        timer.cancel();
    }
}

2.定义完 LiveData 之后,利用它完成页面与ViewModel 间的通信

public class TimerLiveDataActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_timer_live_data);

        final TextView tvTimer = findViewById(R.id.tv_timer);
        final Button btnReset = findViewById(R.id.btn_reset_time);

        // 通过 ViewModelProvider 得到 ViewModel
        TimerLiveDataViewModel timerLiveDataViewModel = new ViewModelProvider(this).get(TimerLiveDataViewModel.class);

        // 得到 ViewModel 中的 LiveData
        MutableLiveData<Integer> liveCurrentSecond = timerLiveDataViewModel.getLiveCurrentSecond();

        // 通过 LiveData.Observer() 观察 ViewModel 中数据的变化
        liveCurrentSecond.observe(this, second -> {
            tvTimer.setText(String.valueOf(second));
        });

        // 重置计时器
        btnReset.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 通过 LiveData.setValue()/LiveData.postValue()
                // 完成对 viewModel 中数据的更新
                liveCurrentSecond.setValue(0);
            }
        });

        //  计时开始
        timerLiveDataViewModel.startTiming();
    }
}

在页面中,通过 LiveData.observe() 方法对 LiveData 所包装的数据进行观察。反过来,当我们希望修改 LiveData 所包装的数据时,也可以通过 LiveData.postValue() / LiveData.setValue()方法来完成。postValue()方法用在非 UI 线程中,若在UI 线程中,则使用 setValue()方法。

image.png

LiveData 的原理

    @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        assertMainThread("observe");
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // ignore
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        owner.getLifecycle().addObserver(wrapper);
    }

从源码可以看出,observer()方法接收的第1个参数是一个 LifecycleOwner 对象,在本例中为 Activity。第 2 个参数是一个 Observer 对象。方法中的最后一行代码将 Observer 与 Activity 的生命周期关联在一起。因此,LiveData 能够感知页面的生命周期,它可以检测页面当前的状态是否为激活状态,或者页面是否被销毁。只有在页面处于激活状态(Lifecycle.State.ON_STARTED 或 Lifecycle.State.ON_RESUME)时,页面才收到来自 LiveData 的通知,若页面被销毁(Lifecycle.State.ON_DESTROY),那么 LiveData 会自动清除与页面的关联,从而避免可能引发的内存泄漏问题。


LiveData.observerForever()方法

LiveData 还提供了一个名为 observerForever() 的方法,使用起来与 observer() 没有太大区别。他们的区别主要在于,当 LiveData 所包装的数据发生变化时,无论页面处于什么状态,observerForever() 都能收到通知。因此,在用完之后,一定要记得调用 removeObserver() 方法来停止对 LiveData 的观察,否则 LiveData 会一直处于激活状态,Activity 则永远不会被系统自动回收,这就造成了内存泄漏。

ViewModel + LiveData 实现Fragment间通信

1、通过上面的内容已经知道,ViewModel 能够将数据从 Activity 中剥离出来。只要 Activity 不被销毁, ViewModel 会一直存在,并且独立于 Activity 的配置变化。旋转屏幕所导致的 Activity 重建,也不会影响到 ViewModel。
2、Fragment 可以被看做 Activity 的子页面,即一个 Activity 中可以包含多个 Fragment。这些 Fragment 彼此独立,但是又都属于同一个 Activity。
3、基于 ViewModel 和 Fragment 组件的这些特性,我们可以巧妙地利用 LiveData,实现同一个 Activity 中的 不同 Fragment 间的通信

示例:
1.定义 ViewModel 和 LiveData。使用 LiveData 对 progress 字段进行包装。

public class ShareDataViewModel extends ViewModel {

    private MutableLiveData<Integer> progress;

    public MutableLiveData<Integer> getProgress() {
        if (progress == null) {
            progress = new MutableLiveData<>();
        }
        return progress;
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        progress = null;
    }
}

2.为了演示方便,将两个 Fragment 等比例放置在 Activity 的布局文件中。也可以分开放置,然后通过 FragmentManager 进行切换。无论怎样,只要保证这两个 Fragment 属于同一个 Activity 即可。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".fragment.FragmentActivity">

    <fragment
        android:id="@+id/fragment_one"
        android:name="com.jinxin.livedata.fragment.OneFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#6200EE"/>

    <fragment
        android:id="@+id/fragment_two"
        android:name="com.jinxin.livedata.fragment.TwoFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

</LinearLayout>

3.编写 Fragment 的布局文件,在其中放置一个 SeekBar 控件。两个 Fragment 的布局文件是类似的

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragment.OneFragment">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_above="@+id/seek_bar"
        android:text="Fragment_one"/>

    <SeekBar
        android:id="@+id/seek_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:max="100"
        android:layout_centerInParent="true"/>
</RelativeLayout>

4.编写 Fragment 的代码,实现具体的通信。这里以 OneFragment 为例, TwoFragment 中的代码与之类似。

public class OneFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_one, container, false);
        initView(view);
        return view.getRootView();
    }

    private void initView(View root) {
        final SeekBar seekBar = root.findViewById(R.id.seek_bar);

        // 注意:这里 ViewModelProvider(getActivity())中的参数
        // 需要的是 Activity,而不是 Fragment, 否则将收不到监听
        ShareDataViewModel shareDataViewModel = new ViewModelProvider(getActivity()).get(ShareDataViewModel.class);

        MutableLiveData<Integer> liveDataProgress = shareDataViewModel.getProgress();

        // 通过 observer 方法观察 ViewModel 中字段数据的变化,并在变化时得到通知
        liveDataProgress.observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer progress) {
                seekBar.setProgress(progress);
            }
        });

        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                // 当用户操作 SeekBar 时,更新 ViewModel 中的数据
                liveDataProgress.setValue(progress);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
            }
        });
    }
}

5.运行程序。

如图所示,无论是滑动 OneFragment 还是 TwoFragment 中的 SeekBar,另一个 Fragment 中的 SeekBar 也会跟着滑动。在滑动 SeekBar 时,通过 LiveData.setValue() 方法,修改了 ViewModel 中 LiveData 包装的数据(progress 字段)。由于 Fragment 通过 LiveData.observer() 方法监听了数据的变化,因此 progress 字段被修改后,Fragment 能够第一时间收到通知并更新 UI。这就是利用 ViewModel 和 LiveData 实现 Fragment 间通信的原理。

image.png

旋转屏幕,SeekBar 的进度与旋转前保持一致,数据并未丢失,如图所示。这是因为 ViewModel 的生命周期独立于页面由于配置发生变化而导致的销毁与重建。

image.png

总结

1.ViewModel 用于存放页面的数据,当数据发送变化时,我们需要通知页面进行更新。在没有 LiveData 之前,可以通过定义接口完成这个需求,有了 LiveData 后,事情变得更加简单、方便。使用 LiveData 对 ViewModel 中我们关系的数据进行包装,并在页面中对其进行监听,当数据发送变化时,页面就能收到通知,进而更新UI
2.LiveData 的本质时观察者模式,并且它能感知页面的生命周期,只在页面存活时才会进行通知,从而避免了内存泄漏。当前你也可以使用 observerForever()方法让 LiveData 忽略页面的生命周期,但用完后,一定要记得使用 removeObserver() 方法移除监听,否则会造成内存泄漏。
3.LiveData 大部分时候是在 ViewModel 中使用的。但它的作用不止于此,LiveData 在其他地方也能发挥重要作用。Jetpack 在设计之初就充分考虑了组件配合使用的需求。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • [TOC] LiveData LiveData概述 LiveData是一个可观察的数据持有者类。与常规observ...
    雪晨杰阅读 1,099评论 0 8
  • 如果你看过了Android架构组件之Lifecycle,可以立马投入到LiveData组件的学习中,同样的,Liv...
    My_Hubery阅读 21,855评论 4 14
  • 绪论 本文是学习了大佬的文章后,自己去动手实践后写的一篇学习笔记。大佬的文章写得比较好,我自己去写未必描述得那么清...
    Lin_YT阅读 1,728评论 0 8
  • 在页面功能较简单的情况下,通常将UI的交互、数据获取等业务全部写在页面中,即通常的MVC模式。但是在页面功能较复杂...
    秀儿2020阅读 555评论 0 3
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,454评论 16 22