livedata使用详解
重点 (Top highlight)
We recently joined a new project with heavy LiveData
usage, everywhere. The search for main thread blockages led us down a rabbit hole of removing a significant portion of our LiveData
usages.
我们最近加入了一个新项目,到处都有大量LiveData
使用。 对主线程阻塞的搜索使我们LiveData
了LiveData
,即删除了大部分LiveData
使用情况。
我们的设置 (Our Setup)
We started off with Repositories, ViewModels and Fragments. The repositories would connect to data sources and expose LiveData
. The ViewModels would then expose the LiveData
to the UI Layer.
我们从存储库,ViewModels和Fragments开始。 存储库将连接到数据源并公开LiveData
。 然后,ViewModels将LiveData
公开给UI层。
Now — there are some issues with this, like the actually unneeded abstraction — but we won’t look at that here. Here’s what a typical Repository would look like:
现在-存在一些问题,例如实际上不需要的抽象-但是我们在这里不再赘述。 这是典型的存储库的外观:
class SupermarketRepository(
private val poiService: POIService,
private val inStockDAO: InStockDAO
) {
fun fetchOpeningHours(id: SupermarketId): LiveData<OpenHoursInformation> {
val liveData = MutableLiveData<OpenHoursInformation>()
val openingHours = poiService.downloadOpeningHours(poiType = SUPERMARKET, id).enqueue(
object: Callback<OpenHoursInformation>() {
override fun onResponse(call: Call<OpenHoursInformation>, response: Response<OpenHoursInformation>) {
liveData.postValue(response.body())
}
...
}
)
liveData.postValue(openingHours)
return liveData
}
fun inStockChanges(itemType: ItemType = ToiletPaper): LiveData<InStockInformation> =
inStockDAO.inStockInformationLiveData()
.map { it.itemType == itemType }
}
From there, we used it in ViewModels:
从那里开始,我们在ViewModels中使用了它:
class HelpViewModel(
private val supermarketRepo: SupermarketRepository
): ViewModel() {
fun fetchSupermarketOpeningHours(id: SupermarketId) =
handleLiveData(
map(supermarketRepo.fetchOpeningHours(id)) { openingHours ->
openingHours.hours.filter { it != null }
}
)
}
The code didn’t look too great, but still okay. There is one big issue here though — and it’s not visible on first sight.
代码看起来不太好,但是仍然可以。 不过,这里有一个大问题-乍一看不到。
handleLiveData
was a simple helper function that took a LiveData
and emitted a loading state before the actual data was emitted. But at some point, we needed to run some transformations on the LiveData
. Creating a MediatorLiveData
every time is quite verbose, but luckily, there is the Transformations
package for LiveData
. It offers methods like map
and switchMap
for LiveData
.
handleLiveData
是一个简单的辅助函数,该函数LiveData
并在发出实际数据之前发出加载状态。 但是在某个时候,我们需要对LiveData
进行一些转换。 每次创建MediatorLiveData
都是非常冗长的,但是幸运的是,这里有LiveData
的Transformations
包 。 它为LiveData
提供诸如map
和switchMap
的LiveData
。
LiveData和线程 (LiveData and Threading)
Soon after joining the project, we realised that we had a lot of Main Thread blocks in our app, so we went to investigate. Looking at our code and looking at LiveData
documentation, we learned that LiveData
observers are always called on the main thread.
加入该项目后不久,我们意识到我们的应用程序中有很多Main Thread块,因此我们进行了调查。 通过查看代码和LiveData
文档,我们了解到LiveData
观察者总是在主线程上调用。
This can be very helpful since LiveData
is often used in the UI Layer. Whenever a new value comes through, you probably want to be on the main thread anyways to display that data. But this threading behavior also shot us in the foot.
由于LiveData
通常在UI层中使用,因此这可能非常有用。 每当有新值通过时,您可能总要在主线程上显示该数据。 但是,这种穿线行为也使我们震惊。
转换方法 (The Transformation Methods)
Looking at the code underneath, our map
function looked like this:
查看下面的代码,我们的map
函数如下所示:
fun <T, R> map(source: LiveData<T>, mapper: (T) -> R): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(source) {
result.value = mapper.invoke(it)
}
return result
}
Coming back to our threading behavior, the callback on line 4 above is called whenever the source
emits a value. In the callback, we assign the MediatorLiveData
‘s value to the result of the mapper
lambda.
回到我们的线程行为,只要source
发出一个值,就会调用上面第4行的回调。 在回调中,我们将MediatorLiveData
的值分配给mapper
lambda的结果。
Since the callback simply is a LiveData
observer, it gets called on the main thread as well. Without actually noticing, we just switched threads! That’s not exactly a great thing: If you are on the main thread, you don’t want to run any long-running operations, if you are on a background thread, you don’t want to do any UI mutations.So in this example, our mapper
lambda gets invoked on the main thread. Just doing some data transformations might not be noticeable, but what if you’re running some more complicated operations? Maybe reading something from the database?
由于回调只是一个LiveData
观察器,因此它也在主线程上被调用。 没有实际注意到,我们只是切换了线程! 这并不是一件好事:如果您在主线程上,则不想运行任何长时间运行的操作,如果您在后台线程上,则不想进行任何UI更改。例如,我们的mapper
lambda在主线程上被调用。 仅执行一些数据转换可能不会引起注意,但是如果您正在运行一些更复杂的操作该怎么办? 也许从数据库中读取某些内容?
class HelpViewModel(
private val supermarketRepo: SupermarketRepository,
private val filterDataDAO: FilterDataDAO
): ViewModel() {
fun fetchSupermarketOpeningHours(id: SupermarketId) =
handleLiveData(
map(supermarketRepo.fetchOpeningHours(id)) { openingHours ->
val filters = filterDataDAO.getFilters()
val filteredHours = openingHours.hours.filter { filter.runFor(it) }
return@map filteredHours
}
)
}
So we learned to be extremely cautious about threading when running transformations on LiveData
. We fixed it afterwards — you can learn how in the next part of this post, coming soon.
因此,我们学会了在LiveData
上运行转换时对线程非常谨慎。 之后,我们对其进行了修复-您可以在即将发布的下一部分中了解如何操作。
这怎么发生的 (How this even happened)
Ideally, this wouldn’t have happened in the first place. The LiveData
documentation clearly states that observers are called on the main thread, so that would have been a great indicator for the previous developer. But sometimes, time-pressure is high, or you might miss something. It happens to everyone and I’m sure there are many codebases out there that look worse than ours.
理想情况下,这根本不会发生。 LiveData
文档明确指出在主线程上调用了观察者,因此对于以前的开发人员而言,这将是一个很好的指示。 但是有时,时间压力很大,否则您可能会错过一些东西。 它发生在每个人身上,我敢肯定那里有很多代码库看起来比我们的差。
The Guide to App Architecture also recommends using LiveData
in Repositories. Lots of developers will have followed this advice, possibly bringing main thread blockages to their apps.
App Architecture指南还建议在存储库中使用LiveData
。 许多开发人员将遵循此建议,可能会将主线程阻塞引入其应用程序。
The other mechanism that should have helped catch this are the Support Annotations. The methods provided by the Transformations
package are annotated with @MainThread
, which tells Android Studio that that method should only be called from the main thread. Similarly, there’s also @WorkerThread
and some more.
支持注释的另一种机制应该是支持注释。 由Transformations
包提供的方法以@MainThread
注释,该方法告诉Android Studio该方法仅应从主线程调用。 同样,还有@WorkerThread
等 。
@MainThread
@NonNull
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;
}
Android Studio will complain if you’re calling a function/method/class that’s also annotated with a Thread Annotation, like this one:
如果您要调用的函数/方法/类还带有线程注释,则Android Studio会抱怨,例如:
@WorkerThread
suspend fun loadAllSupermarkets(radius: Meters) =
poiService.fetchPOIs(poiType = Supermarket, radius = radius.asNumber)
In our case, those annotations weren’t used, so Android Studio didn’t have a chance to catch these mistakes. If you’re doing something slightly funky with threading, or you are unsure about it, it definitely makes sense to use the Thread Annotations to make things more explicit.
在我们的案例中,未使用这些注释,因此Android Studio没有机会抓住这些错误。 如果您正在使用线程做一些时髦的事情,或者不确定该如何做,那么使用“线程注释”使事情变得更加明确无疑是有意义的。
Coming back to the methods offered by the Transformations
package, I believe that it shouldn’t call the transformers on the main thread in any case. Transformations most probably shouldn’t be happening in the UI layer in any case, so running them on the main thread doesn’t make lots of sense.
回到Transformations
包提供的方法,我认为在任何情况下都不应该在主线程上调用转换器。 无论如何,转换很可能都不应该在UI层中进行,因此在主线程上运行它们没有多大意义。
(快速)尝试解决问题 (A (quick) attempt at fixing the issue)
So we needed a quick fix, because who wants to ship apps with main thread blockages?
因此,我们需要快速解决问题,因为谁想发布具有主线程阻塞的应用程序?
A really quick fix would look something like this:
一个非常快速的解决方案如下所示:
fun <T, R> map(source: LiveData<T>, mapper: (T) -> R): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(source) {
AppExecutors.background().execute {
val mappedValue = mapper(it)
AppExecutors.mainThread().execute {
result.value = mappedValue
}
}
}
return result
}
We make sure that we call the mapper
on a background thread and then set the value from the main thread. This might seem a bit complicated but LiveData#setValue
has to be called from the main thread. Alternatively, we could use LiveData#postValue
, but this might lead to unexpected behavior like values being swallowed.Obviously, this is all not great, but it works as a quick fix to help with some main thread blocks.
我们确保在后台线程上调用mapper
,然后从主线程设置值。 这似乎有些复杂,但是必须从主线程调用LiveData#setValue
。 或者,我们可以使用LiveData#postValue
,但这可能会导致意外行为,例如吞下值 。显然,这虽然不好,但是可以作为快速修复方法来帮助解决一些主线程问题。
LiveData和存储库 (LiveData and Repositories)
Apart from the other points, LiveData
is tied to the Android Lifecycle. This can be a great thing, especially when communicating between ViewModel and components like Activities or Fragments.
除了其他方面, LiveData
还与Android生命周期相关联。 这可能是一件好事,尤其是在ViewModel与Activity或Fragments之类的组件之间进行通信时。
The way LiveData
is architected, observing it mostly makes sense from the UI layer. Even when using LiveData#observeForever
which isn’t bound to a lifecycle, the observer is called on the main thread, so that every time you would have to make sure you’re on the right thread for what you want to do.Repositories should be kept free of framework dependencies, and with that also LiveData
since it is tied to the lifecycle.
LiveData
的构建方式,从UI层观察它基本上是有意义的。 即使使用未绑定到生命周期的LiveData#observeForever
,也会在主线程上调用观察器,因此每次您都必须确保您在正确的线程上可以执行您想做的事情。不受框架依赖性的影响,并且与LiveData
因为它与生命周期相关。
流救人! (Flow to the rescue!)
Coroutines Flow are an awesome alternative here. Flow
s offer similar functionality: Builders, cold streams, and useful helpers for e.g. transforming data. Unlike LiveData
, they are not bound to the lifecycle and offer more control over the execution context, which Roman Elizarov wrote a great post about.
协程流在这里是一个很棒的选择。 Flow
提供类似的功能:构建器,冷流以及用于例如转换数据的有用帮助器。 与LiveData
不同,它们不受生命周期的束缚,并提供了对执行上下文的更多控制, Roman Elizarov撰写了一篇很棒的文章 。
Concluding, threading is always something to really look out for. Try to make it explicit which thread you are and should be on, e.g. by using the Thread Annotations. The “real” culprit here is LiveData
though. Not everybody might be aware of the fact that LiveData
observers are always called on the main thread, so it is something to make sure you’re good with when using LiveData
.
最后,线程始终是真正需要注意的事情。 尝试通过使用Thread Annotations明确表明您应该使用哪个线程。 不过,这里的“真正”罪魁祸首是LiveData
。 并不是每个人都知道LiveData
观察者总是在主线程上被调用的事实,因此这可以确保您在使用LiveData
时LiveData
。
With that, calling data transformation methods on the main thread is probably unexpected behavior for most developers and should be made more aware of (which this post hopefully helps with).
这样,对于大多数开发人员而言,在主线程上调用数据转换方法可能是意外的行为,因此应引起更多人的注意(希望此文章对此有所帮助)。
I’d recommend using LiveData
only for communication between ViewModels and the UI Layer where observers being called on the main thread makes more sense.
我建议使用 LiveData
仅用于ViewModels与UI层之间的通信,在主线程上调用观察者更为合理。
Watch this space for the second part — migrating from LiveData
to Coroutines and Flow!
观看此空间的第二部分-从 LiveData
迁移 到Coroutines和Flow!
翻译自: https://proandroiddev.com/dont-use-livedata-in-repositories-f3bebe502ed3
livedata使用详解