The third lecture on the architecture of client-server android-applications, in which we will get acquainted with RxJava and the main operators, and also learn how to create Observable, convert data streams, work with RxJava on Android and solve the problem of Backpressure.
Links to the source code of your solutions you can leave in the comments. Share your decisions with the community, get feedback and constructive criticism. The best solutions will be published on our channel and the website fandroid.info with the authorship of the winners!
Additionally — Backpressure problem
[wpanchor id=»1″]
Introduction
Now we know a lot of ways to do some work in background streams. Under the work here is most often understood server requests. But how good is it with these methods? We must admit that they have many shortcomings.
First, these methods are not flexible. Let’s say you need to execute two requests in parallel, and then wait for both to complete the work, execute the third query using the results of previous queries. If you try to implement such a task using loaders, then most likely you will get a large number of boolean flags, fields to save the result and a lot of code. To the problems of flexibility can also be attributed to the fact that it is difficult to implement a periodic update of data.
Secondly, it is inconvenient to process errors. Again, in the case of loaders, we can return only one result. Of course, you can use special classes that will serve both for data transfer and error transmission, but this is inconvenient.[wpanchor id=»2″]
That’s why we need to consider other possibilities for performing network requests and for working with data. And first of all such possibility is the popular framework RxJava.
RxJava
The RxJava framework allows you to use the functional reactive programming paradigm (FRP) in Android. It is very difficult to understand what this means, therefore, many explanations are required. First, the word functional means that in FRP the basic concept is functions and in this this paradigm is similar to the usual functional programming. Of course, in Android it is very difficult to use full functional programming, but still with RxJava we shift the priority from objects to functions. Second, reactive programming means programming with asynchronous data flows. The easiest way to explain this is in practice: the data stream is any of your requests for a server, data from the database, and the usual input of data from the user (if you speak quite frankly, you can create a data stream from absolutely anything), which is most often performed In the background thread, that is, asynchronously. If we combine two explanations, then we get that functional reactive programming is programming with asynchronous data streams that can be manipulated using various functions.
The definition sounds nice, but it’s completely unclear why it might be needed. It appears, still as can. RxJava allows you to solve almost all the problems that were voiced in the introduction. The main advantages of RxJava are the following:
- Ensuring multithreading. RxJava allows you to flexibly manage the asynchronous execution of queries, as well as switch the execution of operations in different threads. In addition, which is important for Android, RxJava also allows you to easily process the result in the main stream of the application.
- Flow control. This allows you to convert data in a stream, apply operations to data in a stream (for example, save their data from a stream to a database), merge several threads into one, change the stream depending on the result of the other, and much more.
- Error processing. This is another very important advantage of RxJava, which allows you to handle various errors that occur in the thread, repeat server requests in case of errors, and pass errors to subscribers.
And the most pleasant thing is that all the advantages above are achieved literally in a couple of lines of code!
It’s not easy to use RxJava, and using it correctly is even more difficult, and this requires a fairly long study. RxJava is a very large framework for correctly working with it (and in particular with the functional programming paradigm), it is necessary to study and practice a lot. RxJava is worthy of a separate course, there are a huge number of articles on it, and their number is increasing every day. RxJava has already written more than one book, so you can not hope that you can study this framework well in one lecture. But we will try to consider the main features of RxJava and how it can be applied to Android.
We took a little look at the essence of RxJava and functional reactive programming, but in order to move on, we need to know the basic elements of this framework, for example, what is meant by the flow of data and how to create it, what is the subscriber and how to use it, and so on.
RxJava uses the Observer pattern as its basis. What does this pattern look like in classical form? The main entities in it are an object whose value can change, and a subscriber who can subscribe to these changes (each time the value of the object is changed, a certain method will be called from the subscriber). Schematically it can be represented as follows:
The essence of RxJava is almost the same, but instead of one object subscribers use the whole data stream. A subscriber can subscribe to the data stream, and then he will receive information about each new element in the stream, about the errors that occurred, and about the completion of the thread.
Then the scheme for RxJava will be:
With a subscriber, it’s all the less clear, but what is a data stream? The data flow is just a collection of some elements (not necessarily the final one) that are transmitted to the subscriber. As the data stream can act as simple objects and sequences, and infinite, sequences and various events, for example, data entry.
Let’s consider one of the examples that will help to understand exactly where data flows meet and how RxJava can help with working with them. Let’s say the task is to search for people with a specific name when the user enters text in the search field. It can be solved by the following simple lines of code:
editText.observeChanges() .debounce(500, TimeUnit.MILLISECONDS) .map(String::toLowerCase) .flatMap(this::findPerson) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(person -> {}, throwable -> {});
We’ll look at each statement in detail, but for now, just explain what’s happening in the code to see how powerful RxJava is.
First, we turn text input into a data stream, which we will observe. Our thread might look like this: «J», «Jo», «Joh», «John», … This is a data stream.
As already mentioned, we can manage the data in the stream. Let’s say we do not want the query to run too often. To do this, use the debounce operator, which transmits data to the subscriber only when there is a pause between incoming data in the stream (500 milliseconds in our example). Then, for example, our data stream can become like this: «J», «John», …
Next, using the map operator, all the data in the stream turns into lowercase letters. After that the thread looks like this: «j», «john», … [wpanchor id = «3»]
And then a request is made to the server to search for a person with that name. As a result, our data stream of strings turns into a data stream from people and can look like this:
Person{name=”James”, age=25}, Person{name=”John”, age=17}.
Moreover, all this is done in the background thread, and the result is returned to the main application thread. Striking result for 7 lines of code!
Introduction to RxJava
After a brief explanation of the essence of RxJava, let’s go directly to the code and how to work with this framework. In RxJava, the Observable class acts as the data stream. And, as mentioned above, the data stream can be asynchronous, and also various conversion operations can be performed on each element in the stream or even over the entire flow.
Consider the simplest way to create a data stream (Observable) from several elements using the method just:
Observable<Integer> observable = Observable.just(1, 2, 4);
Everything is extremely simple. We created a data stream of 3 elements. Now it remains only to subscribe to it and, for example, to output to the log all the elements:
observable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { // do nothing } @Override public void onError(Throwable e) { // do nothing } @Override public void onNext(Integer integer) { Log.i(TAG, String.valueOf(integer)); } });
It’s hard not to notice that now everything has suddenly ceased to be simple and compact. But we will correct this, but for the time being we will explain what is written here. As you can see, the implementation of the Observer interface acts as a subscriber to the stream. It defines 3 methods. The onNext method is called when the next element from the stream is transferred to the subscriber. The onError and onCompleted methods are called when an error occurs in the data stream and when the data flow ends, respectively.
Now let’s change our code to make it shorter and more pleasant to read. It can be seen that we do not use onError and onCompleted methods, so we do not need them. Fortunately, the subscribe method has many different forms, and they allow you to use only the methods you need. For example, in this way, you can only handle the onNext call:
Observable<Integer> observable = Observable.just(1, 2, 4); observable.subscribe(integer -> Log.i(TAG, String.valueOf(integer)));
But this code is already much shorter and more understandable. And we, moreover, do not lose the ability to handle errors, we only need to pass this handler to the second parameter:
observable.subscribe( integer -> Log.i(TAG, String.valueOf(integer)), throwable -> {/*handle error*/});
Of course, the situation with onComplete is completely analogous.
But now a very interesting and fair question. This, of course, is cool, that we used data streams, subscribers and RxJava’s power, but we just output the data to the log. I can make it a bit simpler:
int[] items = new int[]{1, 2, 4}; for (int value : items) { Log.i(TAG, String.valueOf(value)); }
And this, of course, is correct. To show the superiority of RxJava, let’s arrange a small competition, where each task will be implemented both with the help of RxJava, and with the help of loops.
Task 1: Given a set of powers of two from 1 to 64 inclusive, each of them must be output to the log as in the previous example. In this case, the call to String.valueOf looks ugly, it needs to be rewritten so that the data in the stream / array turns into strings.
//RxJava Observable.just(1, 2, 4, 8, 16, 32, 64) .map(String::valueOf) .subscribe(value -> Log.i(TAG, value)); //For int[] items = new int[]{1, 2, 4, 8, 16, 32, 64}; for (int value : items) { String s = String.valueOf(value); Log.i(TAG, s); }
There is no noticeable advantage to RxJava here. So we go further.
Task 2: It is necessary to change the data so that only numbers from 13 and above fall into the log:
//RxJava Observable.just(1, 2, 4, 8, 16, 32, 64) .filter(integer -> integer >= 13) .map(String::valueOf) .subscribe(value -> Log.i(TAG, value)); //For int[] items = new int[]{1, 2, 4, 8, 16, 32, 64}; for (int value : items) { if (value >= 13) { String s = String.valueOf(value); Log.i(TAG, s); } }
And although there is no noticeable difference in the amount of code, I must say that the code with RxJava looks cleaner, more logical and understandable. Let’s get the ways of working through loops the next task.
Task 3: all data operations must be performed in the background thread, and the data output to the log is in the main application thread:
//RxJava Observable.just(1, 2, 4, 8, 16, 32, 64) .filter(integer -> integer >= 13) .map(String::valueOf) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(value -> Log.i(TAG, value)); //For EXECUTOR.execute(() -> { int[] items = new int[]{1, 2, 4, 8, 16, 32, 64}; for (int value : items) { if (value >= 13) { String s = String.valueOf(value); HANDLER.post(() -> Log.i(TAG, s)); } } });
The solution of the previous task in 2 lines — here it is, the true power of RxJava when working with threads (while the code remains much cleaner and more understandable). And when using cycles, we had to create Executor to work in the background and Handler to transfer the data to the main application thread. [Wpanchor id = «4»]
Even such toy examples show that RxJava has the advantages to use. Although, of course, to put it bluntly, RxJava and cycles are things that can not be compared, since these are completely different paradigms.
Let’s continue our acquaintance with RxJava and its various operators. And start by considering how to create data streams.
Creating an Observable
We’ve already seen using the just method to create the Observable. Approximately similar is the from method, which allows you to create a data stream from the list of items:
List<Integer> values = new ArrayList<>(); values.add(5); values.add(10); values.add(15); values.add(20); return Observable.from(values);
In fact, all methods of creating data streams eventually call the standard method Observable.create. Therefore, we consider this method. In fact, when using this approach, we control which data to send to the subscriber, and we call the onNext, onError, and onCompleted methods ourselves. A simple example of creating Observable through create:
@NonNull public static Observable<Integer> observableWithCreate() { return Observable.create(subscriber -> { subscriber.onNext(5); subscriber.onNext(10); try { //stub long-running operation Thread.sleep(300); } catch (InterruptedException e) { subscriber.onError(e); return; } subscriber.onNext(15); subscriber.onCompleted(); }); }
As you can see in the example, we manually transfer the data to the subscriber. In this case, such a data stream will be given to each subscriber who will subscribe to the created Observable.
Use the create method directly to developers is not recommended. Let’s say a few words about this now, and more will be in the extra lecture on the problem of Backpressure. In the same lecture, other ways of creating Observable will be considered.
Why is the create method inconvenient? First, you need to handle all potential errors correctly and transfer them to the subscriber yourself. Secondly, you must always ensure that the subscriber is still subscribed to the data stream. For example, consider the subscription code for Observable, which was described above:
Subscription subscription = RxJavaCreate.observableWithCreate() .subscribeOn(Schedulers.newThread()) .subscribe(System.out::println); subscription.unsubscribe();
In this code, the subscriber subscribes to receive data and immediately unsubscribes from them. And the Observable code waits 300ms before sending the last element to the subscriber, and at the time of the transfer, the subscriber has unsubscribed from receiving the data. Therefore, an error occurs. Therefore, the code in the create method needs to be modified by adding a check to the fact that the subscriber still needs data from the stream:
@NonNull public static Observable<Integer> observableWithCreate() { return Observable.create(subscriber -> { subscriber.onNext(5); subscriber.onNext(10); try { //stub long-running operation Thread.sleep(300); } catch (InterruptedException e) { subscriber.onError(e); return; } if (!subscriber.isUnsubscribed()) { subscriber.onNext(15); } subscriber.onCompleted(); }); }
At the same time, this check should be added before each call onNext, onError and onCompleted (here it is omitted for convenience), which is inconvenient and often leads to errors. Therefore, you do not need to use the create method.
In addition to directly creating a data stream, it is important to know how to perform operations on data in the background thread, and how to process the result in a specific thread. For this purpose, the operators subscribeOn and observeOn serve, in which developers are often confused, just starting their acquaintance with RxJava.
The subscribeOn method is used to specify the thread in which the code is run to create data in the Observable. If you look from the point of view of the code, the code in the call method in the onSubscribe interface will be executed in the thread that was passed to the subscribeOn method. The observeOn method specifies the stream in which the data is to be processed by the subscriber. You can remember that the subscriber is an observer and you specify where exactly he needs to observe the observeOn method. And the remaining method is used to indicate where the data stream should work.
It’s not entirely correct to say that in the methods observeOn and subscribeOn we specify threads, no — we pass in them Scheduler instances, which, among other things, take on the task of scheduling tasks. You can create your own Scheduler, but you usually use the standard ones:
- Io () — execution of tasks that do not heavily burden the processor, but are long. This, for example, calls to the server or to the database. The size of the thread pool is unlimited.
- Computation () — execution of computational tasks. The size of this pool is equivalent to the number of processor cores.
- NewThread () creates a new thread for each task.
- Immediate () — execution of the task in the same thread from which the Observable is called. Most often it is used for testing.
In addition, the RxAndroid library has AndroidSchedulers.mainThread (), which transfers the execution of the subscriber code to the main thread. [wpanchor id = «5»]
Based on the explanations it is clear that most often you will use Schedulers.io () for queries to the server and database, and in AndroidSchedulers.mainThread () will process the result. Therefore, the code for most Observable will look like this:
return Observable.just(1) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread());
Basic Operators
Consider the basic operators that you can use to convert data in a stream. Each of these operators takes a function as a value — in this, the paradigm of functional reactive programming is realized.
We have already seen some of the operators. Of course, this is primarily a map operator, which converts an element in a stream to any other element. Its work can be visually represented using the following scheme:
We already used the map operator earlier to convert a number to a string. This is not the only application, you can convert anything into anything, the main thing is that it should be according to the logic of your application.
I need to say one more thing: if several operations are required to convert one object to another, it is better to use several map operators than to write several operations in one statement. This will greatly improve the readability of your code. That is, instead of this code:
return observable .map(integer -> { int value = integer * 2; String text = String.valueOf(value); return text.hashCode(); }); Лучше использовать такой: return observable .map(integer -> integer * 2) .map(String::valueOf) .map(String::hashCode);
The second version of the code is not the only correct one or the faster one, but it is easier to understand and better consistent with the principles of functional reactive programming.
Most of these operators are either simple or not very often used, so for the following operators, we give only a brief description with a schema and without code examples.
Also popular is the filter operator, which leaves only data in the stream that satisfy the condition passed to the parameter.
There are also other methods for filtering data in the stream, for example, skip, take, first and others:
There is still a very large number of different operators, a full description and the scheme of which can be found in the documentation, so there is no sense in listing them here.
In the Observable class, many standard and useful operators are defined. But what if you want to somehow transform Observable, and standard operators allow you to do this in a few actions or with extra lines of code? For example, for each network request, you will probably write the following code to control the flow:
return Observable.just(1) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread());
And this, we emphasize once again, will be for every request. It would be logical if we could use our operator for this task. For such purposes, the Transformer interface is used. Create your own transformer for the data stream:[wpanchor id=»6″]
public class AsyncTransformer<T> implements Observable.Transformer<T, T> { @Override public Observable<T> call(Observable<T> observable) { return observable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } }
And now we can use this transformer in a universal way:
@NonNull public static Observable<Integer> async() { return Observable.just(1) .compose(new AsyncTransformer<>()); }
Converting Data Streams
In addition to directly converting data in a stream, you can manage the flow itself: for example, when a certain condition occurs, you can replace one stream with another, or you can combine the execution of several threads together or sequentially. For this, there is another large group of operators.
To execute several Observable consecutively, you can use the concat method, whose schema looks like this:
And in code this method can be used like this:
Observable<Integer> first = Observable.just(1, 4, 8); Observable<Integer> second = Observable.just(2, 6, 9); Observable<Integer> third = Observable.just("Red", "Hello").map(String::length); return Observable.concat(first, second, third);
Likewise, if you need to execute several Observables not sequentially, but in parallel (which is often necessary to speed up the load when running multiple requests), then you can use the merge operator:
In this case, the order of receipt of the elements is not defined. This method is used similarly to the concat method:
Observable<Integer> first = Observable.just(1, 4, 8); Observable<Integer> second = Observable.just(2, 6, 9); Observable<Integer> third = Observable.just("Red", "Hello").map(String::length); return Observable.merge(first, second, third);
It must also be said that both the concat method and the merge method require that the data in the Observable be of the same type, which is not always convenient.
There is also a more interesting method for parallel execution of queries, which also allows you to process the results of all threads together and convert the data in the right way. This is a zip method that takes an input list of Observable, which will be executed in parallel, as well as a function for converting data from all queries:
And its use in the code:
Observable<String> names = Observable.just("John", "Jack"); Observable<Integer> ages = Observable.just(28, 17); return Observable.zip(names, ages, Person::new);
In this example, one data stream is created from the names of people, one more of the ages, then these threads are executed in parallel and converted into a stream of people using data from both source streams.
But, probably, one of the most popular operators is the flatMap operator. It is similar to the map operator, but with a small exception. If map is for converting an object to an object, flatMap converts each object into a data stream into an Observable, and then merges all the resulting data streams. The scheme of this operator is as follows:
The flatMap operator is often used for various transformations over threads, for example, to return an error, if incorrect data came or something went wrong as intended. We ourselves used this example in the last lecture, when we processed the change in the status of the request. Now we can parse this code fragment with a lot of understanding:
RxSQLite.get().querySingle(RequestTable.TABLE, where) .compose(RxSchedulers.async()) .flatMap(request -> { if (request.getStatus() == RequestStatus.IN_PROGRESS) { mLoadingView.showLoadingIndicator(); return Observable.empty(); } else if (request.getStatus() == RequestStatus.ERROR) { return Observable.error(new IOException(request.getError())); } return RxSQLite.get().querySingle(CityTable.TABLE).compose(RxSchedulers.async()); })
First of all, it should be noted that we get only one element in the Observable with statuses, and on the basis of it we change the data flow.
First, if the status of the IN_PROGRESS request, then we no longer need to continue executing the query, so we return an empty data stream. Secondly, if an error occurred (this is an example where no explicit Exception occurs, but a logical error occurred), we return the Observable with an error. Finally, if the query succeeds, we change the data stream with the status of a request for a data stream with a weather forecast. Here, flatMap demonstrates all its power.
This is probably the main and most commonly used operators. However, again it must be said that there are still a very large number of different operators, each of which is well suited for a particular case.