Jetpack Architecture - LiveData

简介

LiveData 是一个数据持有类,基于观察者模式机制,使能数据监测功能。具体来说,被LiveData包裹的数据可以被其他观察者订阅,当该数据发生变化时,观察者能立即得到通知,获取到最新的值。另外,LiveData具备生命周期感知功能,因此只有当系统组件(比如ActivityFragmentServiceLifecycleOwner)处于激活状态(即状态为Lifecycle.State.STARTEDLifecycle.State.RESUMED)时,才能接收到数据更新通知,而当系统组件处于销毁状态(即Lifecycle.State.DESTROY)时,会自动取消订阅,因此可以有效避免内存泄漏问题。这也是LiveData最大的两个特点。

特性

使用LiveData有如下几个优点:

  • 响应式数据LiveData采用观察者模式,当底层数据改变时,会自动通知注册组件,注册组件可以直接将数据渲染到界面上。

  • 具备生命周期感知LiveData底层使用了Lifecycle组件,因此可以感知系统组件(确切来说是LifecycleOwner)生命周期,在系统组件销毁时,自动取消订阅,防止内存泄漏。

  • 数据分发与否与组件当前状态相关:只有当系统组件处于激活状态时,数据更新才会进行通知,也即当系统组件处于后台或销毁状态时,即使数据改变了,也不会通知到相关系统组件,而当系统组件从后台转到前台时,LiveData会自动将当前最新数据通知给到系统组件,因此组件能永远渲染最新数据。

  • 多组件共享数据:多个不同的组件可注册到同一个LiveData中,这样可以实现数据共享功能。

依赖导入

要使用LiveData,只需添加如下依赖:

dependencies {
    def lifecycle_version = "2.3.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

如果项目已引入appcompat包,则无需再单独引入LiveData

基本使用

LiveData最大的特性就是使能数据具备观察通知功能,并且支持任意数据更新通知,因此只需将数据交给LiveData持有,且需要数据的组件注册一下LiveData进行监听即可,具体如下所示:

// 数据类
data class User(val name: String, val age: Int)

// LiveData 设置数据(无需考虑所在线程)
fun <T> MutableLiveData<T>.updateValue(value: T) {
    fun isMainThread(): Boolean {
        return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            Looper.getMainLooper().isCurrentThread
        } else {
            Looper.getMainLooper() == Looper.myLooper()
        }
    }

    if (isMainThread()) {
        this.value = value
    } else {
        this.postValue(value)
    }
}

// 系统组件
class MainActivity : AppCompatActivity() {

    private companion object {
        private const val TAG = "MainActivity"
    }

    // LiveData: 数据持有类
    private val reactiveData by lazy {
        MutableLiveData<User>()
    }

    // 更新数据
    private fun updateData() {
        val value = (1..60).random()
        val user = User("user:$value", value)
        this.reactiveData.updateValue(user)
        Log.v(TAG, "update data: $user")

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 使能数据监听
        this.reactiveData.observe(this) {
            // 将最新的数据渲染到界面上
            this.tv.text = it.toString()
        }

        // 点击更新数据
        this.btn.setOnClickListener {
            this.updateData()
        }
    }
}

LiveData是一个抽象类,其主要的子类有MutableLiveDataMediatorLiveData,通常使用MutableLiveData即可:

public abstract class LiveData<T> {...}
public class MutableLiveData<T> extends LiveData<T> {...}
public class MediatorLiveData<T> extends MutableLiveData<T> {...}

上述例子定义一个数据类User,然后在系统组件MainActivity中持有一个LiveData实例reactiveData,可以使能对User数据类的监听功能,最后在onCreate方法中采用LiveDate#observe(LifecycleOwner, Observer)MainActivity注册数据监听功能,这样,后续如果reactiveData内部数据User被修改了,则MainActivity就能监听到,并在Observer#onChanged(T)回调方法中获取得到最新值。

:前文已介绍过,LiveData具备页面生命周期感知功能,只有在页面组件处于激活状态时,才能接收数据更新通知,而如果需要实现一直接收LiveData数据更新通知,无论当前页面处于哪种状态,则可以使用LiveData#observeForever(Observer)方法进行注册,需要注意的是,当组件退出时,需要手动调用LiveData#removeObserver(Observer)方法移除组件监听,避免组件被持有,造成内存泄漏。

LiveData提供了两种方法更新数据:LiveData#setValue(T)LiveData#postValue(T),其中:

  • setValue(T):用于在主线程中更新数据
  • postValue(T):用于在工作线程中更新数据

之所以提供了两种方法更新数据,是为了保证组件永远在主线程中获取到最新值(即Observer#onChanged(T)回调永远发生在主线程中),可以直接渲染界面。

上述例子为MutableLiveData添加了一个扩展函数MutableLiveData#updateValue(T),让我们修改数据时无需考虑所在线程,操作更加方便。

ViewModel 结合 LiveData

LiveData主要是使能数据监听功能,而ViewModel主要用于持有数据,保证数据在相关组件的生命周期内唯一且一直可用。这两者结合使用,就可以很好的让系统组件获取到对应的数据且能自动监听到数据的修改,本质上其实就是实现了「响应式数据」,在数据更新后,可以自动将最新的数据直接渲染到界面上,解耦了数据与界面,开发过程中,只需关注数据操作即可,大大降低开发复杂度。

从 Android 官方推荐的 MVVM 架构图可以更清晰的知道两者的关系:

android MVVM architecture

可以看到,在 MVVM 架构中,ViewModel内部持有一个或多个LiveData实例,每个LiveData会使能内部数据更新通知功能,因此,系统组件只需获取其ViewModel内部的相关LiveData,注册监听数据,这样就可以自动获取最新数据并直接渲染到界面上。

举个例子:我们更改下前面的例子,将LiveData放置到一个ViewModel中,并将数据操作等方法置于ViewModel中,然后让系统组件MainActivityViewModel中获取数据并进行监听。具体步骤如下:

  1. 自定义一个ViewModel,提供数据操作方法:

    class UserViewModel : ViewModel() {
        private companion object {
            private const val TAG = "UserViewModel"
        }
    
        // LiveData
        val reactiveData by lazy {
            MutableLiveData<User>()
        }
    
        // 更新数据
        fun updateData() {
            val value = (1..60).random()
            val user = User("user:$value", value)
            this.reactiveData.updateValue(user)
            Log.v(TAG, "update data: $user")
        }
    }
    
  2. 系统组件创建所需数据相应的ViewModel,获取内部LiveData实例并注册数据监听功能:

    // 系统组件
    class MainActivity : AppCompatActivity() {
    
        // ViewModel
        private val userViewModel by viewModels<UserViewModel>()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // 使能数据监听
            this.userViewModel.reactiveData.observe(this) {
                // 将最新的数据渲染到界面上
                this.tv.text = it.toString()
            }
    
            // 点击更新数据
            this.btn.setOnClickListener {
                this.userViewModel.updateData()
            }
        }
    }
    

以上,当我们点击界面按钮更新数据时,最新的数据会自动渲染到界面TextView上。

:上述例子中MainActivity可以直接获取得到ViewModel内部的LiveData,这样MainActivity可以直接使用这个LiveData实例设置更改数据,破坏了ViewModel数据的封装性,一种解决思路是让ViewModel向外只暴露一个只读LiveData,具体如下所示:

class UserViewModel : ViewModel() {
    // 对外暴露只读 LiveData
    val reactiveData: LiveData<User>
        get() = _reactiveData

    // 底层可读可写 LiveData
    private val _reactiveData by lazy {
        MutableLiveData<User>()
    }

    // 更新数据
    fun updateData() {
        val value = (1..60).random()
        val user = User("user:$value", value)
        this._reactiveData.updateValue(user)
        Log.v(TAG, "update data: $user")
    }
    ...
}

对外暴露的是LiveDataLiveDatasetValue(T)postValue(T)都是protected,因此即使外部获取到LiveData,也不能进行数据修改等操作。

合并 LiveData

通常一个LiveData持有一个数据,而当一个页面需要多个数据时,就需要注册到多个LiveData中,这样会导致页面组件代码臃肿,此时可借助MediatorLiveData,它可以合并多个LiveData实例,当任意一个LiveData底层数据改变时,都会通知到页面组件,而页面组件只需注册一次(即注册到MediatorLiveData中)。

举个例子:比如现在页面MainActivity需要两个数据源,而这个两个数据分别由liveData1liveData2持有,此时我们可以合并liveData1livedata2,然后MainActivity只需注册到合并的LiveData中,即可实现数据监听。具体步骤如下所示:

  1. 创建一个ViewModel,持有两个数据源liveData1liveData2,合并这两个LiveData并暴露给外部组件:

    inline val <reified T> T.TAG: String
        get() = "Why8n"
    
    class MyViewModel : ViewModel() {
    
        // 数据源1
        private val liveData1 = MutableLiveData<String>() 
    
        // 数据源2
        private val liveData2 = MutableLiveData<String>() 
    
        // 合并数据源(此处向外暴露为 LiveData 类型,外部只能进行读取,不能进行设置)
        val liveDataMerged: LiveData<String> = MediatorLiveData<String>().apply {
            // 完整写法
            this.addSource(this@MyViewModel.liveData1, object : Observer<String> {
                override fun onChanged(str: String?) {
                    this@apply.value = str
                }
            })
    
            // Lambda简化写法
            this.addSource(this@MyViewModel.liveData2) {
                this.value = it
            }
        }
    
        // 更新数据
        fun updateData() {
            val value = (0..100).random()
            if (value % 2 == 0) {
                Log.v(TAG, "liveData1 update: $value")
                this.liveData1.value = "liveData1 update: $value"
            } else {
                Log.v(TAG, "liveData2 update: $value")
                this.liveData2.value = "liveData2 update: $value"
            }
        }
    }
    

    其实就是通过MediatorLiveData#addSource(LiveData<T> source, Observer<? super S> onChanged)方法为MediatorLiveData添加合并数据源,当源数据source底层数据改变时,MediatorLiveData会得到通知,因此其onChanged函数会得到回调,而我们在onChanged函数中手动调用MediatorLiveData#setValue(T)source的最新值赋值给到MediatorLiveData,因此,注册到MediatorLiveData的外部页面组件就会得到通知...这样就完成了一个数据传递过程。

  2. 页面组件源码如下所示:

    class MainActivity : AppCompatActivity() {
    
        // ViewModel
        private val myViewModel by viewModels<MyViewModel>()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            this.myViewModel.liveDataMerged.observe(this) {
                this.tv.text = it
                Log.v(TAG, "MainActivity receives: $it")
            }
    
            // 点击更新数据
            this.btn.setOnClickListener {
                this.myViewModel.updateData()
            }
        }
    }
    

    以上,当源数据liveData1liveData2任意一个数据更新时,MainActivity都能得到通知。

数据转换

有时候,LiveData的底层数据类型并不是页面组件所需的直接类型,当然我们可以通过在页面组件内手动将类型进行转换,但是其实LiveData本身已提供了一些数据转换操作,大致有如下几种:

  • Transformations.map():将源LiveData中的数据转换成其他类型数据,然后包装到一个新的LiveData中。

    Transformations#map()函数的源码如下所示:

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

    map()的源码很简单,其实内部就是创建了一个MediatorLiveData,并绑定到了当前的源LiveData中,当源LiveData改变时,MediatorLiveData就会接收到并将数据通过mapFunction进行转换,这样外部组件就能接收到已转换的类型数据。

    举个例子:比如源数据为LiveData<User>,现在假设页面组件MainActivity需要的数据为User的 JSON 字符串类型,即LiveData<String>类型,那么借助Transformations#map(),就可以很轻易完成数据类型转换:

    class UserViewModel : ViewModel() {
    
        // 源数据
        private val _reactiveData by lazy {
            MutableLiveData<User>()
        }
    
        // 目标 LiveData
        val jsonUser: LiveData<String> = Transformations.map(this._reactiveData) { user: User ->
            "{\"name\": \"${user.name}\", \"age\": ${user.age} }"
        }
        ...
    }
    

    外部组件只需注册到目标LiveData即可:

    class MainActivity : AppCompatActivity() {
    
        private val userViewModel by viewModels<UserViewModel>()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // 注册到目标 LiveData
            this.userViewModel.jsonUser.observe(this) {
                this.tv.text = it.toString()
            }
            ...
        }
    }
    

    以上,则当源数据_reactiveData底层数据改变时,目标LiveData(即jsonUser)会自动感知到,并自动将数据进行转换,通知给到页面组件,完成一轮数据自动转换并渲染过程。

  • Transformations#switchMap():将源LiveData中的数据转换为一个新的LiveData,当该新LiveData数据改变时,外部组件也能监听到。

    switchMap()map()的区别之处在于:switchMap()的转换函数(即第二个参数)类型为Function<X, LiveData<Y>>,而map()函数的转换函数类型为Function<X, Y>,因此,switchMap()会将源LiveData最新数据转换为一个新的LiveData

    switchMap()的原理其实也很简单,与map()函数一致,底层都是通过创建一个MediatorLiveData作为桥接,具体源码如下所示:

    @MainThread
    public static <X, Y> LiveData<Y> switchMap(
            @NonNull LiveData<X> source,
            @NonNull final Function<X, LiveData<Y>> switchMapFunction) {
    
        // 创建一个 MediatorLiveData
        final MediatorLiveData<Y> result = new MediatorLiveData<>();
        // 添加源 LiveData
        result.addSource(source, new Observer<X>() {
            LiveData<Y> mSource;
    
            @Override
            public void onChanged(@Nullable X x) {
                // 转换函数生成一个新的 LiveData
                LiveData<Y> newLiveData = switchMapFunction.apply(x);
                // 新旧为同一个 LiveData,则无需处理
                if (mSource == newLiveData) {
                    return;
                }
                // 移除旧的 LiveData
                if (mSource != null) {
                    result.removeSource(mSource);
                }
                mSource = newLiveData;
                if (mSource != null) {
                    // 添加新的 LiveData
                    result.addSource(mSource, new Observer<Y>() {
                        @Override
                        public void onChanged(@Nullable Y y) {
                            result.setValue(y);
                        }
                    });
                }
            }
        });
        return result;
    }
    

    switchMap()map()都是返回一个MediatorLiveData,外部组件通过订阅该MediatorLiveData即可间接监听到源LiveData数据变更,switchMap()由于转换函数每次都返回一个新的LiveData,因此需要进行移除旧LiveData和注册新LiveData等操作,这些都已经由MediatorLiveData默认处理了,因此,外部组件无需关心这些底层操作,直接注册到switchMap()函数返回的MediatorLiveData即可。

    举个例子:假设UserViewModel内部持有一个源LiveData<String>,装载用户名称,现有一个接口getUser(String),可通过用户名称返回一个完整的用户信息,类型为LiveData<User>,主页面需要获取该User实例数据进行渲染。UserViewModel的大致内容如下:

    class UserViewModel : ViewModel() {
    
        // 源 LiveData
        private val nameLiveData by lazy {
            MutableLiveData<String>()
        }
    
        fun getUser(name: String): LiveData<User> {
            val age = (1..60).random()
            val user = User(name, age)
            return MutableLiveData<User>(user)
        }
    }
    

    由于getUser()每次都返回一个新的LiveData<User>对象,因此,主页面MainActivity不能直接注册到这个LiveData中,在每一次调用getUser()前,必须先手动移除旧的LiveData,然后再注册到这个新的LiveData中,这样才能继续完成数据监听功能,手动如此操作略显繁琐,此时可借助Transformations#switchMap()函数,直接一步到位:

    class UserViewModel : ViewModel() {
    
        // 源 LiveData
        private val nameLiveData by lazy {
            MutableLiveData<String>()
        }
    
        // 目标 LiveData
        val userLiveData: LiveData<User> =
            Transformations.switchMap(this.nameLiveData) { name: String ->
                this.getUser(name)
            }
    
        private fun getUser(name: String): LiveData<User> {
            val age = (1..60).random()
            val user = User(name, age)
            return MutableLiveData<User>(user)
        }
    
        // 更新数据
        fun updateData(name: String) {
            this.nameLiveData.value = name
        } 
    }
    

    通过Transformations#switchMap()函数,后续只要源LiveData数据改变了,switchMap()就会感知到,然后将最新的数据进行转换,生成一个新的LiveData,主页面只需一次注册到switchMap()返回的LiveData中,即可监听到数据变更。主页面源码如下所示:

    class MainActivity : AppCompatActivity() {
    
        // ViewModel
        private val userViewModel by viewModels<UserViewModel>()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            this.userViewModel.userLiveData.observe(this) {
                this.tv.text = it.toString()
            }
    
            // 点击更新数据
            this.btn.setOnClickListener {
                this.userViewModel.updateData("user: ${(1..60).random()}")
            }
        }
    }
    

参考

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

推荐阅读更多精彩内容