livedata使用详解_不要在存储库中使用LiveData

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使用。 对主线程阻塞的搜索使我们LiveDataLiveData ,即删除了大部分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都是非常冗长的,但是幸运的是,这里有LiveDataTransformations 。 它为LiveData提供诸如mapswitchMapLiveData

LiveData和线程 (LiveData and Threading)

Image for post
Freepik from Freepikwww.flaticon.com, then lovingly put together in Google Drawings by me. www.flaticon.com制作的Android图标,然后由我精心地组合到Google绘图中。

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. Flows 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撰写了一篇很棒的文章

Image for post
Where we’d put our Flows! :)
我们将流程放到哪里! :)

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观察者总是在主线程上被调用的事实,因此这可以确保您在使用LiveDataLiveData

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使用详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值