LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures that LiveData only updates app component observers that are in an active lifecycle state.
LiveData是可观察的数据持有者类。 与常规的可观察数据不同,LiveData具有生命周期感知功能,这意味着它尊重其他应用程序组件(例如活动,片段或服务)的生命周期。 这种意识确保LiveData仅更新处于活动生命周期状态的应用程序组件观察者。
To understand LiveData, we first need to understand the observer pattern. It is a software design pattern in which an object (called the subject) maintains a list of its dependents (called observers) and notifies them automatically of any state changes, usually by calling one of their methods.
要了解LiveData,我们首先需要了解观察者模式。 它是一种软件设计模式,其中,一个对象(称为主题)维护其依赖项列表(称为观察者),并通常通过调用其方法之一来自动将状态更改通知它们。
If you don’t know about LiveData or how it works, please go through this piece before proceeding further for a better understanding.
如果您不了解LiveData或其工作方式,请仔细阅读本部分,然后再继续进行进一步的了解。
The common use case of observing LiveData is to show a snack bar or to perform some custom action like showing a dialog, navigation, etc. For example, let’s consider a simple app where there are two activities, with each having its own ViewModel. Upon observing the live data value, the first activity navigates to the second. But the problem is when the user presses the back button in the second activity or when the user resumes the first activity from background state, then the live data value gets reposted to the first activity, which again navigates to the second activity. As a result, the user will never be able to come back to the first activity.
观察LiveData的常见用例是显示小吃店或执行一些自定义操作,例如显示对话框,导航等。例如,让我们考虑一个简单的应用程序,其中有两个活动,每个活动都有自己的ViewModel。 观察实时数据值后,第一个活动将导航到第二个活动。 但是问题在于,当用户在第二个活动中按下“后退”按钮时,或者当用户从后台状态恢复第一个活动时,实时数据值将重新发布到第一个活动中,然后再次导航到第二个活动。 结果,用户将永远无法返回到第一个活动。
So here’s the point: How do we create LiveData that emits a single event and notifies only a single observer even though there are a number of observers subscribed to it?
因此,关键是:我们如何创建一个LiveData来发出一个事件并仅通知一个观察者,即使有许多观察者订阅了它?
There is a wrapper class called LiveEvent that solves these kinds of scenarios. It is a LiveData that will only send an update once. It is an extension of MediatorLiveData and is handy when updating a single observer while many observers are observing it.
有一个名为LiveEvent的包装器类,可以解决这类情况。 这是一个LiveData,只会发送一次更新。 它是MediatorLiveData的扩展 并且在许多观察者正在观察的情况下更新单个观察者时非常方便。
import androidx.annotation.MainThread import androidx.collection.ArraySet import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.Observer class LiveEvent<T> : MediatorLiveData<T>() { private val observers = ArraySet<ObserverWrapper<in T>>() @MainThread override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { val wrapper = ObserverWrapper(observer) observers.add(wrapper) super.observe(owner, wrapper) } @MainThread override fun removeObserver(observer: Observer<in T>) { if (observers.remove(observer as Observer<*>)) { super.removeObserver(observer) return } val iterator = observers.iterator() while (iterator.hasNext()) { val wrapper = iterator.next() if (wrapper.observer == observer) { iterator.remove() super.removeObserver(wrapper) break } } } @MainThread override fun setValue(t: T?) { observers.forEach { it.newValue() } super.setValue(t) } private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> { private var pending = false override fun onChanged(t: T?) { if (pending) { pending = false observer.onChanged(t) } } fun newValue() { pending = true } } }Let’s understand it better with an example
让我们通过一个例子更好地理解它
For example, let’s consider a simple TV show app where there is an activity that has two fragments. FragmentShowsList displays the list of shows, and upon the click of the show in FragmentShowsList, we will be navigating to FragmentShowDetails, which displays the show details. But what if there was a common button action in both fragments (e.g. SUBSCRIBE) where, upon the click of that button, we hit the subscription API and post the result using LiveData? As the same LiveData instance is being observed in both fragments upon receiving the result, we need to show a success dialog in the case of FragmentShowsList and toast message in the case of FragmentShowDetails.
例如,让我们考虑一个简单的电视节目应用程序,其中有一个包含两个片段的活动。 FragmentShowsList显示节目列表,在FragmentShowsList单击节目后,我们将导航到FragmentShowDetails ,该节目将显示节目详细信息。 但是,如果两个片段中都有一个通用的按钮动作(例如SUBSCRIBE ),该怎么办? 单击该按钮后,我们在哪里点击订阅API并使用LiveData发布结果? 作为相同LiveData实例在两个片段在接收到该结果观察到,我们需要显示在的情况下,成功对话框FragmentShowsList中的情况下和烤面包消息FragmentShowDetails 。
But the problem comes as we are observing a single ViewModel instance. If FragmentShowsList is on top, it receives the result and shows dialog. But later if we navigate to FragmentShowDetails, it will show the toast message without clicking on the button because it’s observing the LiveData that contains a value.
但是问题出在我们观察单个ViewModel实例时。 如果FragmentShowsList位于顶部,它将接收结果并显示对话框。 但是稍后,如果我们导航到FragmentShowDetails ,它将显示敬酒消息而无需单击按钮,因为它正在观察包含值的LiveData。
Our activity will look like this:
我们的活动将如下所示:
package com.example.viewmodel import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider class MainActivity : AppCompatActivity() { lateinit var viewModel: SampleViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(SampleViewModel::class.java) viewModel.navigationDetails.observe(this, Observer { if(it == true){ attachShowDetailsFragment() } }) attachShowsFragment() } private fun attachShowsFragment() { supportFragmentManager .beginTransaction() .replace(R.id.container, FragmentShowsList(),FragmentShowsList::class.java.canonicalName ) .addToBackStack(FragmentShowsList::class.java.canonicalName) .commit() } private fun attachShowDetailsFragment() { supportFragmentManager .beginTransaction() .replace(R.id.container, ShowDetailsFragment(),ShowDetailsFragment::class.java.canonicalName ) .addToBackStack(ShowDetailsFragment::class.java.canonicalName) .commit() } }Its XML looks like this:
其XML如下所示:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/container" />Now let's create the fragment_shows_list layout file:
现在让我们创建fragment_shows_list 布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20dp" android:textSize="24sp" android:text="Shows List Fragment"/> <Button android:id="@+id/btn_subscribe" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Subscribe" android:textColor="#000000" android:padding="20dp"/> <Button android:id="@+id/btn_navigation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Navigate" android:textColor="#000000" android:padding="20dp"/> </LinearLayout>Next, let’s create FragmentShowsList:
接下来,让我们创建FragmentShowsList :
package com.example.viewmodel import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import kotlinx.android.synthetic.main.fragment_shows_list.* class FragmentShowsList :Fragment() { lateinit var viewModel: SampleViewModel @CallSuper override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_shows_list, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel = ViewModelProvider(activity!!).get(SampleViewModel::class.java) btn_subscribe?.setOnClickListener { viewModel.subscribe() } btn_navigation?.setOnClickListener { viewModel.addSecondFragment() } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.subscriptionDetails.observe(this, Observer { showSuccessDialog() }) } private fun showSuccessDialog() { activity?.let { val builder = AlertDialog.Builder(it) builder.setMessage("Successfully subscribed to the course") .setPositiveButton("OK", DialogInterface.OnClickListener { dialog, id -> dialog.dismiss() }) // Create the AlertDialog object and return it builder.create() builder.show() } ?: throw IllegalStateException("Activity cannot be null") } }Next, let’s create FragmentShowDetails:
接下来,让我们创建FragmentShowDetails :
package com.example.viewmodel import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.annotation.CallSuper import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider class FragmentShowDetails : Fragment() { lateinit var viewModel: SampleViewModel @CallSuper override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_shows_details, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel = ViewModelProvider(activity!!).get(SampleViewModel::class.java) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.subscriptionDetails.observe(this, Observer { Toast.makeText(activity!!,"Successfully subscribed",Toast.LENGTH_SHORT).show() }) } }And fragment_shows_details:
和fragment_shows_details :
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:gravity="center" android:background="#ffffff" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="This is Show Details Fragment" android:textColor="#000000" android:textSize="35sp" android:textStyle="bold" tools:text="This is second Activity" /> </LinearLayout>Now let us run and check the output:
现在让我们运行并检查输出:
I clicked the subscribe button on FragmentShowList. On the result, it showed the success dialog acknowledgment, which is expected.
我单击FragmentShowList上的订阅按钮。 结果显示成功对话确认,这是预期的。
When I clicked on the navigation button, it navigated to FragmentShowDetails but unexpectedly also displayed a toast message that was not expected or required.
当我单击导航按钮时,它导航到FragmentShowDetails但出乎意料的是,它还显示了不希望或不需要的祝酒消息。
When I clicked BackPress on FragmentShowDetails, it was replaced with FragmentShowList but showed a success dialog that was not required or a case of duplication.
当我在FragmentShowDetails上单击BackPress时,将其替换为FragmentShowList但显示了一个不需要的成功对话框或出现重复的情况。
To communicate between two different fragments inside an activity, we commonly use the shared ViewModel of an activity to eliminate boilerplate code with an interface. But scenarios like the one above cause unexpected behaviors to arise if not dealt with correctly.
为了在活动中的两个不同片段之间进行通信,我们通常使用活动的共享ViewModel来消除带有接口的样板代码。 但是,如果未正确处理,则上述情况会导致意外行为的发生。
Now let's add the LiveEvent class to our code, change two lines of code in the ViewModel, and see the magic:
现在,将LiveEvent类添加到我们的代码中,在ViewModel中更改两行代码,然后看一下魔术:
There was nothing to change in the code for the activity or fragments — just a simple change on two lines of code in the ViewModel:
活动或片段的代码没有任何更改,只是ViewModel中两行代码的简单更改:
package com.example.viewmodel import androidx.lifecycle.ViewModel class SampleViewModel : ViewModel() { private val _subscriptionStatus = LiveEvent<Boolean>() private val _navigation= LiveEvent<Boolean>() val navigationDetails : LiveEvent<Boolean> get() = _navigation val subscriptionDetails : LiveEvent<Boolean> get() = _subscriptionStatus fun subscribe() { //Do something here _subscriptionStatus.postValue(true) } fun addSecondFragment() { _navigation.postValue(true) } }The reason for the magic above is MediatorLiveData. It can be defined as a LiveData subclass that may observe other LiveData objects and react to OnChanged events from them. In this LiveEvent class, we are wrapping the observer with an ObserverWrapper class where the actual logic has been written. There is a boolean value to tell whether the change is observed or not at least once.
产生上述魔力的原因是MediatorLiveData 。 它 可以定义为一个LiveData子类,该子类可以观察其他LiveData对象并对其中的OnChanged事件做出React。 在此LiveEvent类中,我们用ObserverWrapper包装ObserverWrapper 类 编写实际逻辑的位置。 有一个布尔值可以告诉您是否至少观察到一次更改。
private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> { private var pending = false override fun onChanged(t: T?) { if (pending) { pending = false observer.onChanged(t) } } fun newValue() { pending = true } }That’s all for now. So who has faced a similar issue with LiveData? Please comment. I hope you have found a good workaround.
目前为止就这样了。 那么谁在LiveData上遇到过类似的问题呢? 请评论。 希望您找到了一个好的解决方法。
Please let me know your suggestions.
请让我知道您的建议。
Thanks for reading.
谢谢阅读。
翻译自: https://medium.com/better-programming/what-is-liveevent-livedata-7270a64736b3