Android reactive programming basics

1. Introduction to reactive programming


Developing a complex Android application with many network connections, user interaction, and animation means writing code that is full of nested callbacks. And as the project develops, such a code becomes not only cumbersome and difficult to understand, but also difficult to develop, maintain, and prone to many subtle errors.

ReactiveX or functional reactive programming offers an alternative approach that can significantly reduce application code and create elegant understandable applications for managing asynchronous tasks and events. In reactive programming, the consumer responds to the data as it arrives and propagates the event changes to the registered observers.

RxJava is an open source implementation of ReactiveX in Java. The basic building blocks of reactive code are Observables and Subscribers. More information on the basic framework can be found in the article Grokai * RxJava, part one: the basics .

RxAndroid is an extension to RxJava, which allows the scheduler to run code in the main and additional threads of the Android application and provides the transfer of results from the created additional threads to the main one for aggregation and interaction with the user interface.
In order to better understand the basic principles of reactive programming, consider a practical example for the Android platform. And let's start by setting up the development environment.

2. Preparation of the environment


We connect the main libraries and write the dependencies in the dependencies {} section of the buil.gradle configuration file:
dependencies { 
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.1.6' 
}

We include support for lambda expressions - we use the new features of the Java 8 language on the Android N platform. To use the features of the Java 8 language, you also need to connect the new Jack compiler, for which add to the build.gradle file:
android {
	...
  	defaultConfig {
    	...
    	jackOptions {
      		enabled true
    	}
  	}
  	compileOptions {
    		sourceCompatibility JavaVersion.VERSION_1_8
    		targetCompatibility JavaVersion.VERSION_1_8
  	}
}

Note: Jack is only supported in Android Studio 2.1 and you also need to upgrade to JDK 8.

When you make changes to the gradle configuration file, a warning appears that you need to synchronize the project and, to apply all the changes, click on the Sync Now link in the upper right.

3. Create a basic example


Due to the fact that the use of RxAndroid in most cases is associated with projects with multi-threaded processing of network connections - consider a simple example of processing the results of site parsing.
To display the results, create a simple layout:

For parsing, create a simple WebParsing class with two getURLs and getTitle methods:
public class WebParsing {
public List getURLs(String url) {
   Document doc;
   List stringList = new ArrayList<>();
   try {
       doc = Jsoup.connect(url).get();
       Elements select = doc.select("a");
       for (Element element : select) {
           stringList.add(element.attr("href"));
       }
   } catch (IOException e) {
       e.printStackTrace();
       return null;
   }
   return stringList;
}
}

public String getTitle(String url) {
   String title;
   try {
       Document doc = Jsoup.connect(url).get();
       title = doc.title();
   } catch (MalformedURLException mue) {
       mue.printStackTrace();
       return null;
   } catch (HttpStatusException hse) {
       hse.printStackTrace();
       return null;
   } catch (IOException e) {
       e.printStackTrace();
       return null;
   } catch (IllegalArgumentException iae) {
       iae.printStackTrace();
       return null;
   }
   return title;
}

The getURLs method scans the content of the site and returns a list of all the links found, and the getTitle method returns the Title of the site by the link.

4. We connect reactivity


In order to use the capabilities of RxAndroid based on the above methods, we will create two corresponding Observables:
Observable> queryURLs(String url) {
   WebParsing webParsing = new WebParsing();
   return Observable.create(
           new Observable.OnSubscribe>() {
               @Override
               public void call(Subscriber> subscriber) {
                   subscriber.onNext(webParsing.getURLs(url));
                   subscriber.onCompleted();
               }
   }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}

Observable queryTitle(String url) {
   WebParsing webParsing = new WebParsing();
   return Observable.create(new Observable.OnSubscribe() {
       @Override
       public void call(Subscriber subscriber) {
           subscriber.onNext(webParsing.getTitle(url));
           subscriber.onCompleted();
       }
   }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}

The first Observable will generate a list of URL links found on the site, the second will generate a Title. Let's analyze the example of the feather method in detail and line by line:
  1. Observable queryURLs (String url) - the line declares the Observable method, which takes as an input parameter a link to the site for parsing and returns the result of parsing as a list of links from the specified site;
    WebParsing webParsing = new WebParsing () - creates a variable to access our parsing functions;
    return Observable.create - creates an Observable that returns a list of links;
    new Observable.OnSubscribe()- the line declares the OnSubscribe interface with one method (see below), which is called upon subscription;
    public void call (Subscriber subscriber) - overloads the call method, which will be called after the Subscriber subscription;
    subscriber.onNext (webParsing.getURLs (url)) - calls the onNext method to transmit Subscriber data whenever data is generated. This method takes as a parameter the object emitted by the Observable;
    subscriber.onCompleted () - Observable calls the onCompleted () method after it calls onNext for the last time if no errors were detected;
    subscribeOn (Schedulers.io ()) - the subscribeOn method subscribes all Observable upstream to the Schedulers.io () scheduler;
    observeOn (AndroidSchedulers.mainThread ()) - observeOn method allows you to get the result in the main application thread.

5. Launch the first reactive application


So, Observables are created, we implement the simplest example based on the first method above, which will display a list of site links:
public void example0(final TextView textView, String url) {
   queryURLs(url)
           .subscribe(new Action1>() {
               @Override
               public void call(List urls) {
                   for (String url: urls) {
                       String string = (String) textView.getText();
                       textView.setText(string + url + "\n\n");
                   }
               }
           });
}

Wrap our implemented example in the MainExample class and call it in MainActivity:
public class MainActivity extends AppCompatActivity {
   TextView textView;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       textView = (TextView) findViewById(R.id.textView);
       MainExample mainExample = new MainExample();
       mainExample.example0(textView, "https://yandex.ru/");
   }
}

6. Increasing reactivity - using operators


Observable can transform output using operators and they can be used between Observable and Subscriber to manipulate data. There are a lot of operators in RxJava, so for a start let's look at the most popular ones.
And to begin with, get rid of the loop in the subscriber and force the observer to sequentially emit the data from the received array of links, and the from () operator will help us in this:
public void example1(final TextView textView, String url) {
   queryURLs(url)
           .subscribe(new Action1>() {
               @Override
               public void call(List urls) {
                   Observable.from(urls)
                           .subscribe(new Action1() {
                               @Override
                               public void call(String url) {
                                   String string = (String) textView.getText();
                                   textView.setText(string + url + "\n\n");
                               }
                           });
               }
           });
}

It doesn’t look quite beautiful and a bit confusing, so we use the following flatMap () operator, which receives data emitted by one Observable and returns data emitted by another Observable, thus replacing one Observable with another:
public void example2(final TextView textView, String url) {
   queryURLs(url)
           .flatMap(new Func1, Observable>() {
               @Override
               public Observable call(List urls) {
                   return Observable.from(urls);
               }
           })
           .subscribe(new Action1() {
                               @Override
                               public void call(String url) {
                                   String string = (String) textView.getText();
                                   textView.setText(string + url + "\n\n");
                               }
                           });
}

In the next step, unload our Subscriber and use the map () operator, through which you can convert one data item to another. The map () operator can also transform data and generate data of the type we need, different from the original one. In our case, the observer will form a list of lines, and the subscriber will only display them on the screen:
public void example3(final TextView textView, String url) {
   queryURLs(url)
           .flatMap(new Func1, Observable>() {
               @Override
               public Observable call(List urls) {
                   return Observable.from(urls);
               }
           })
           .map(new Func1() {
               @Override
               public String call(String url) {
                   return textView.getText() + url + "\n\n";
               }
           })
           .subscribe(new Action1() {
               @Override
               public void call(String url) {
                   textView.setText(url);
               }
           });
}

We examined the main features and now it's time to use lambdas to simplify our code:
queryURLs(url)
       .flatMap(urls -> Observable.from(urls))
       .map(url1 -> textView.getText() + url1 + "\n\n")
       .subscribe(url1 -> {
           textView.setText(url1);
       });

or even simpler:
queryURLs(url)
       .flatMap(Observable::from)
       .map(url1 -> textView.getText() + url1 + "\n\n")
       .subscribe(textView::setText);

Compare the construction above with the resulting code and feel the power and simplicity of lambda expressions.

7. We increase power


In the next step, we complicate our processing and use the flatMap () operator to connect the second prepared queryTitle () method, which also returns an observer. This method returns the Title of the site by the link to the site. Let's create an example in which we will create and display a list of website headers using the links found on the web page, i.e. instead of the received list of links to sites in the previous example, we display the titles of these sites:
public void example4(final TextView textView, String url) {
   queryURLs(url)
           .flatMap(new Func1, Observable>() {
               @Override
               public Observable call(List urls) {
                   return Observable.from(urls);
               }
           })
           .flatMap(new Func1>() {
               @Override
               public Observable call(String url) {
                   return queryTitle(url);
               }
           })
           .subscribe(new Action1() {
               @Override
               public void call(String title) {
                   textView.setText(title);
               }
           });
}

or in abbreviated form:
queryURLs(url)
       .flatMap(Observable::from)
       .flatMap(this::queryTitle)
       .subscribe(textView::setText);

add map () to create a list of headers:
queryURLs(url)
       .flatMap(Observable::from)
       .flatMap(this::queryTitle)
       .map(url1 -> textView.getText() + url1 + "\n\n")
       .subscribe(textView::setText);

using the filter () operator, we filter out empty lines with a null value:
queryURLs(url)
       .flatMap(Observable::from)
       .flatMap(this::queryTitle)
       .filter(title -> title != null)
       .map(url1 -> textView.getText() + url1 + "\n\n")
       .subscribe(textView::setText);

using the take () operator, we take only the first 7 headers:
queryURLs(url)
       .flatMap(Observable::from)
       .flatMap(this::queryTitle)
       .filter(title -> title != null)
       .take(7)
       .map(url1 -> textView.getText() + url1 + "\n\n")
       .subscribe(textView::setText);

The last example showed that the combination of many methods plus the use of a large number of available operators plus lambda expressions and we get a powerful processor of various data streams from just a few lines.

All examples given in the article are posted here .

Sources:


  1. Official documentation
  2. Grokai * RxJava, part one: the basics
  3. Getting Started With ReactiveX on Android
  4. RxJava - Tutorial
  5. Getting Started with RxJava and Android
  6. Reactive Programming with RxJava in Android
  7. Party tricks with RxJava, RxAndroid & Retrolambda

Also popular now: