rxjava背压怎样使用
by Nikita Kozlov
由Nikita Kozlov
是否想优化网络使用? 检查本地存储和RxJava背压 (Want to optimize network usage? Check out local storage and RxJava backpressure)
Users love fast, responsive apps. They don’t want to hear how API calls take time. They want to see updates immediately. Right now. Once you meet that expectation, users start interacting with the app more and more. It’s so easy for them, since the app responds immediately.
用户喜欢快速响应的应用程序。 他们不想听到API调用如何花费时间。 他们想立即查看更新。 现在 。 一旦您达到了期望,用户就会开始与该应用进行越来越多的交互。 对于他们来说,这很容易,因为该应用程序会立即响应。
With all this positive impact on the business comes increased usage of the network and the battery. So it’s in everyone’s best interest to minimize the number of network calls.
所有这些对业务的积极影响是网络和电池使用率的提高。 因此,最小化网络呼叫的数量符合每个人的最大利益。
In this article, I’ll share how one such case was solved using RxJava.
在本文中,我将分享如何使用RxJava解决这种情况。
To make it more interesting, I’ve added an optional challenge along with the solution. One of the branches in the repo has only a few base classes and a set of acceptance tests. Before reading the solution, you can try to solve the problem on your own by making those tests pass. You’ll find more information in the “Challenge” section below.
为了使其更加有趣,我在解决方案中添加了一个可选的挑战。 回购中的一个分支只有几个基本类和一组验收测试。 在阅读解决方案之前,您可以通过使这些测试通过来尝试自行解决问题。 您可以在下面的“挑战”部分中找到更多信息。
问题 (The Problem)
The task is to develop a feature that allows users to add and remove items from a certain list. The list is stored on the back end. Many apps face this problem in one way or another: marking an e-mail as important in Gmail, adding a song to your favorites on Spotify, or recommending an article here, on Medium (?).
任务是开发一种功能,允许用户从特定列表中添加和删除项目。 该列表存储在后端。 许多应用以一种或多种方式面临此问题:在Gmail中将电子邮件标记为重要,在Spotify上将歌曲添加到收藏夹中,或在中(?)上推荐本文。
The problem sounds simple, but it gets trickier when things like connection latency and network errors are taken into account.
这个问题听起来很简单,但是当考虑到连接延迟和网络错误之类的问题时,问题就变得棘手了。
Implementation must fulfil the following requirements:
实现必须满足以下要求:
The user interface reacts immediately to the user’s actions. Users want to see the results of their actions immediately. If we can’t synchronize those changes, we should notify our users, and roll back to the previous state.
用户界面会立即对用户的动作做出React。 用户希望立即查看其操作结果。 如果我们无法同步这些更改,则应通知我们的用户,并回滚到以前的状态。
Interaction from multiple devices is supported. This doesn’t mean that we need to support changes in real time — but we do need to fetch the whole collection from time to time. Plus, our back end provides us with API endpoints for additions and removals, which we must use to support better synchronization.
支持来自多个设备的交互。 这并不意味着我们需要实时支持更改,而是需要不时获取整个集合。 另外,我们的后端为我们提供了用于添加和删除的API端点,我们必须使用它们来支持更好的同步。
Integrity of the data is guaranteed. Whenever a synchronization call fails, our app should recover gracefully from that error.
保证数据的完整性。 每当同步调用失败时,我们的应用应从该错误中正常恢复。
The architectural decisions are discussed in a separate article: “How to leverage Local Storage to build lightning-fast apps”. This article is focused on optimizing the number of calls to the backend by using RxJava.
在另一篇文章中讨论了体系结构决策:“ 如何利用本地存储构建快速的应用程序 ”。 本文重点介绍通过使用RxJava优化对后端的调用次数。
正式定义 (Formal definition)
We need to develop the following interface:
我们需要开发以下接口:
interface ItemRepository { Single<List<? extends Item<ItemId>>> getItemList();
Single<Response> addItem(ItemId id);
Single<Response> removeItem(ItemId id);
Observable<Integer> getCounter();
boolean hasItem(ItemId id);}
The methods addItem()
and removeItem()
can be called in any order with any arguments. To avoid passing unnecessary information, both methods need only ItemId
, while getItemList()
returns complete items.
方法addItem()
和removeItem()
可以以任何顺序以任何参数调用。 为了避免传递不必要的信息,这两种方法都只需要ItemId
,而getItemList()
返回完整的项目。
Please take into account that getCounter()
returns an Observable
that emits an amount of items in the collection any time a change happens to the ItemRepository
. This means that, for example, we increase the counter every time a new item is added, even though synchronization with the back end is not yet finished.
请注意, ItemRepository
发生更改时, getCounter()
返回的Observable
都会在集合中发出一定数量的项目。 这意味着,例如,即使尚未完成与后端的同步,每次添加新项时我们都会增加计数器。
Back end API is the following:
后端API如下:
public interface Api { Single<List<? extends Item<ItemId>>> getItemList();
Single<ApiResponse> addItem(ItemId id);
Single<ApiResponse> removeItem(ItemId id);}
We can add or remove items by their IDs and fetch the whole collection.
我们可以通过ID添加或删除项目,并获取整个集合。
挑战 (Challenge)
In order to make it more interesting, here is the promised challenge. It is, of course, optional, so you can go directly to the “Solution” section if you prefer.
为了使它变得更有趣,这是所承诺的挑战。 当然,它是可选的,因此您可以根据需要直接转到“解决方案”部分。
Inspired by the article “Practical Challenges For RxJava Learners” by Sergii Zhuk, I decided to create a separate branch with only basic classes and acceptance tests. You can make them pass by yourself and then compare your solution with mine. In total, there are 29 tests. They are split into four parts according to the functionality covered:
受Sergii Zhuk的文章“ RxJava学习者的实际挑战”的启发,我决定创建一个仅包含基本类和验收测试的独立分支 。 您可以让他们自己通过,然后将您的解决方案与我的解决方案进行比较。 总共有29个测试。 根据所涵盖的功能,它们分为四个部分:
Fetching: Fetching the list from API and checking what it contains.
提取:从API提取列表并检查其中包含的内容。
Adding and removing: Checking that proper API methods are called and state of the repository is changed accordingly.
添加和删除:检查是否调用了正确的API方法以及相应地更改了存储库的状态。
Counter changes: Some screens display the total amount of items in the list. That functionality is tested in this part.
计数器更改:某些屏幕显示列表中的项目总数。 该功能已在本部分中进行了测试。
Backend calls optimization: Even though the user might add/remove a single item as many times as they want, the network should not be overworked.
后端呼叫优化:即使用户可以根据需要添加/删除单个项目多次,也不应使网络工作过度。
There is more than one way to optimize backend calls. So in case you’re not sure about tests, check out the “Optimization Strategy” section.
优化后端调用的方法不止一种。 因此,如果您不确定测试,请查看“优化策略”部分。
解 (Solution)
To solve this problem I decided to build the ItemRepository
in the following way:
为了解决此问题,我决定以以下方式构建ItemRepository
:
Along with a local copy of the main list, local storage has two extra lists: one for ongoing additions, and one for ongoing removals. They help it recover from negative cases. To read more about it, please check the following article: How to leverage Local Storage to build lightning-fast apps.
除了主列表的本地副本外,本地存储还有两个额外的列表:一个用于正在进行的添加,另一个用于进行中的删除。 他们帮助它从负面案件中恢复过来。 要了解更多信息,请查看以下文章: 如何利用本地存储构建快速的应用程序 。
优化策略 (Optimization Strategy)
We should not launch all the requests to the backend at once. For example, if you add and remove the same item simultaneously, the result is unpredictable. We don’t know which request would be the first to finish. So, operations on a single item should be queued and launched one by one. Requests that deal with different items can go in parallel without problems.
我们不应该一次向后端启动所有请求。 例如,如果您同时添加和删除同一项目,则结果是不可预测的。 我们不知道哪个请求最先完成。 因此,对单个项目的操作应排入队列并逐个启动。 处理不同项目的请求可以并行处理而不会出现问题。
Queues are executed in parallel. Each queue consists of requests for adding and removing a particular item. Let’s take a look at one of them.
队列是并行执行的。 每个队列由添加和删除特定项目的请求组成。 让我们看看其中之一。
The most important requests are the latest to come and the last request that was launched. If they are the same — for example, they’re both “add” — then we can skip the whole queue and do nothing. If they are different, then once the current request has finished, you can launch the latest one and skip everything in the middle.
最重要的请求是最新的请求,也是最后一个已启动的请求。 如果它们相同(例如,它们都是“添加”的),那么我们可以跳过整个队列,什么也不做。 如果它们不同,则当前请求完成后,您可以启动最新的请求,并跳过中间的所有请求。
Also, ItemRepository
should start syncing with the back end, without any artificial delays, as soon as it has new data. Buffering and batching can be used to further improve this solution, but it is out of the scope of this article. I'm mentioning it here so you won't be surprised by the solution’s behavior.
同样,一旦有新数据, ItemRepository
应该立即与后端同步,而没有任何人为延迟。 缓冲和批处理可用于进一步改进此解决方案,但这不在本文的讨论范围之内。 我在这里提到它,因此您不会对解决方案的行为感到惊讶。
RxJava来了 (Here comes RxJava)
RxJava provides powerful mechanisms to manipulate data streams. Let’s use them to our advantage.
RxJava提供了强大的机制来处理数据流。 让我们利用它们发挥我们的优势。
First, let’s create events that represent user intention: Add
and Remove
. Now, using Subject
or Relay
, we can create an input stream of those events.
首先,让我们创建代表用户意图的事件: Add
和Remove
。 现在,使用Subject
或Relay
,我们可以创建这些事件的输入流。
Second, we’ll implement queues by splitting this stream into multiple sub-streams of events grouped by the same itemId
. To do this, we can use the groupBy
operator that returns an Observable
of GroupedObservable
. Each GroupedObservable
is a queue from the optimization strategy.
其次,我们将通过将该流划分为由同一itemId
分组的事件的多个子流来实现队列。 为此,我们可以使用groupBy
运算符,该groupBy
符返回GroupedObservable
的Observable
。 每个GroupedObservable
是优化策略中的一个队列。
We don’t want any of those sub-streams to complete. But if something unexpected happens, we want to be sure that data will continue to flow no matter what. And groupBy
operator can help us with that. If any of the GroupedObservable
are terminated, perhaps because of an error, a new one is created when an event with a matching itemId
is in the stream.
我们不希望任何这些子流完成。 但是,如果发生意外情况,我们希望确保无论如何数据都会继续流动。 而groupBy
运算符可以帮助我们。 如果GroupedObservable
中的任何一个终止,可能是由于错误而终止,则在流中具有匹配itemId
的事件时会创建一个新的。
After this stage, we have a set of sub-streams that we are going to optimize independently.
在此阶段之后,我们将拥有一组要独立优化的子流。
优化 (Optimization)
The third and final step is optimization. Before we start, let’s check the optimization strategy once more. It says that we need to keep the most recent event along with the last one that actually caused a network call. We can drop all the events in the middle! They just don't matter.
第三步也是最后一步是优化。 在开始之前,让我们再次检查优化策略。 它说我们需要保留最近的事件以及实际上引起网络呼叫的最后一个事件。 我们可以将所有事件放到中间! 他们没关系 。
Unfortunately, we don't know how long each back-end request takes or how often a new event is emitted. This means that we can't use window
, buffer
, throttle
, or debounce
to implement the required behavior. To use these, we need to know timing beforehand.
不幸的是,我们不知道每个后端请求花费多长时间或发出新事件的频率。 这意味着我们不能使用window
, buffer
, throttle
或debounce
来实现所需的行为。 要使用这些,我们需要事先知道时间。
We need each GroupedObservable
to emit a new event only when an ongoing call is finished. In other words, once the call is finished, we need to ask GroupedObservable
to emit a new item. That can be done by using reactive pull backpressure. So we need to create the operator onBackpressureLatest(Subscriber)
. It drops all events except the last one that is emitted only when the Subscriber.request()
method is called.
我们需要每个GroupedObservable
仅在正在进行的调用结束时才发出新事件。 换句话说,一旦调用结束,我们需要让GroupedObservable
发出一个新项。 这可以通过使用反作用力拉回压力来完成。 因此,我们需要在onBackpressureLatest(Subscriber)
上创建运算符。 它将删除所有事件,只有最后一个仅在调用Subscriber.request()
方法时发出的事件除外。
Now let’s do the back-end call in the Subscriber
so it knows when to call request()
. It can also handle the comparison between events. If they are the same, then it skips the last one. Otherwise, it launches a call to the back end. The result is posted to the output stream.
现在,让我们在Subscriber
进行后端调用,以便它知道何时调用request()
。 它还可以处理事件之间的比较。 如果它们相同,则跳过最后一个。 否则,它将向后端发起呼叫。 结果将发布到输出流。
Methods ItemRepository.addItem()
and ItemRepository.removeItem()
return Single
, so we need to filter this stream to deliver proper responses to each of the callers of our API.
方法ItemRepository.addItem()
和ItemRepository.removeItem()
返回Single
,因此我们需要过滤此流以向API的每个调用者传递正确的响应。
请求的第三种状态 (The third state of the request)
Independently of skipping requests or making them, consumers of ItemRepository
expect to get responses. This means that even if some requests are skipped, we still need to return as many responses as calls for addItem()
and removeItem()
are made. There are two ways to deal with this.
与跳过请求或发出请求ItemRepository
, ItemRepository
希望获得响应。 这意味着即使跳过某些请求,我们仍然需要返回与对addItem()
和removeItem()
调用一样多的响应。 有两种解决方法。
First, we could hide all optimizations from the consumer and act as if all the requests have been executed. This is good for encapsulation, because after performing optimizations, you don’t need to change consumers’ code.
首先,我们可以向使用者隐藏所有优化,并像执行所有请求一样进行操作。 这对于封装很有用,因为执行优化后,您无需更改使用者的代码。
But in some cases that means more complexity. For example, if we show a snackbar for each response, and the end-user clicked multiple times to add and remove the same item, we will spam him with notifications. To resolve this issue, we have to implement some logic for dealing with such cases.
但是在某些情况下,这意味着更加复杂。 例如,如果我们为每个响应显示一个小吃栏,并且最终用户多次单击以添加和删除同一项目,则我们将通过通知向他发送垃圾邮件。 为了解决这个问题,我们必须实现一些逻辑来处理这种情况。
Or we could go for the second approach. We could be honest with consumers of our API and add a third, “skipped,” state of the response. From my experience, I can say that it makes things simpler. In the example above, we just wouldn’t show notifications for all skipped requests.
或者我们可以选择第二种方法。 我们可以对API的使用者诚实,然后添加响应的第三个“跳过”状态。 根据我的经验,我可以说它使事情变得简单。 在上面的示例中,我们只是不显示所有跳过请求的通知。
编码时间 (Coding time)
In pseudo-code, it looks simpler than it sounds:
用伪代码,看起来比听起来简单:
Let’s go through most important parts:
让我们来看看最重要的部分:
Line 1: We grouped
inputStream
by ID and got our sub-streams.第1行:我们按ID对
inputStream
进行了分组,并获得了子流。- Line 3: We applied Backpressure. Everything but the last event is dropped at this stage. 第3行:我们施加了背压。 除了最后一个事件,所有其他内容都将在此阶段删除。
- Line 6: Depending on the event, we either skip it and return the skipped response, or launch a network call and save it for future comparison. 第6行:根据事件,我们要么跳过它并返回跳过的响应,要么启动一个网络呼叫并保存以供将来比较。
Please note that code above won’t compile. You can find the actual code and a small Android app to play with in the repo.
请注意,上面的代码不会编译。 您可以在回购中找到实际的代码和一个小型Android应用程序。
结论 (Conclusion)
Even a routine task like removing and adding items to a collection becomes a challenge when it comes to optimization, synchronization, and excellent user experience. Good tools, like RxJava, can help us keep code concise and expressive.
在优化,同步和出色的用户体验方面,甚至是一项日常任务(如将项目删除和添加到集合中)都将成为一项挑战。 像RxJava这样的好工具可以帮助我们保持代码的简洁和表达力。
You can find my solution here in the module switchman
. Along with it, in the module app
, you can find an example application to play with. If you are interested in a challenge, check out the branch “challenge” — the tests are in the module switchman
. For more information, check the section “Show me the code” in README.
你可以找到我的解决方案在这里在模块switchman
。 伴随着它,在模块app
,您可以找到一个示例应用程序。 如果您对挑战感兴趣,请查看“挑战”分支-测试位于模块switchman
。 有关更多信息,请参见README中的“向我显示代码”部分。
Nikita Kozlov (@Nikita_E_Kozlov) | TwitterThe latest Tweets from Nikita Kozlov (@Nikita_E_Kozlov): “https://t.co/wmGSJ7snW1"twitter.com
Nikita Kozlov(@Nikita_E_Kozlov)| Twitter 来自Nikita Kozlov(@Nikita_E_Kozlov)的最新推文:“ https://t.co/wmGSJ7snW1” twitter.com
Thank you for you time reading this article. If you like it, don’t forget to click the ? below.
感谢您抽出宝贵的时间阅读本文。 如果喜欢,不要忘记单击“ ?”。 b低。
rxjava背压怎样使用