Skip to content

8. RxJava in the Swing Environment

8.1. Introduction

Here, we will revisit the Swing application presented in Section 2.

  

To work with RxJava in a Swing environment, we will use the RxSwing library, which adds classes and interfaces to RxJava that are useful in a Swing environment. To do this, the Gradle file for the Swing example is as follows:

  

buildscript {
    repositories {
        mavenCentral()
    }
}
apply plugin: 'java'
jar {
    baseName = 'exemples-01'
    version = '0.0.1-SNAPSHOT'
}
repositories {
    mavenCentral()
}
dependencies {
    compile('io.reactivex:rxswing:0.25.0')
    compile('io.reactivex:rxjava:1.1.3')
    compile('com.fasterxml.jackson.core:jackson-databind:2.7.3')
}
task wrapper(type: Wrapper) {
    gradleVersion = '2.9'
}
  • line 15: the dependency on RxSwing;

We will use only a single RxSwing-specific object: the scheduler [SwingScheduler.getInstance()], which executes/observes observables on the Swing event loop thread. We will use it exclusively to observe observables running on threads other than the event loop. Let’s review the architecture of the example application:

Image

  • the asynchronous service layer provides methods that return observables. We execute these observables in threads other than the event loop thread. This way, the GUI remains responsive. It can react to user input. The most obvious example is allowing the user to click a [Cancel] button to interrupt an asynchronous operation that is taking too long. For this to work, the GUI must be frozen;
  • the Swing layer needs to process the results returned by asynchronous operations and use them to update the GUI. However, this can only be done in the event loop thread. To achieve this, these results are observed in the scheduler [SwingScheduler.getInstance()];

Thus, in the GUI event handling code, interaction with the asynchronous layer [rxService] takes the following form:


Observable obs=rxService.doSomething(...).subscribeOn(Schedulers.computation()).observeOn(SwingScheduler.getInstance()) ;

where the scheduler [Schedulers.computation()] can be replaced by another scheduler depending on the use case.

The reader is invited to reread paragraph 2. They now have the knowledge to fully understand it.

8.2. The code structure

The code implements the following architecture:

Image

The IntelliJ IDEA project implementing this architecture is as follows:

  
  • the [rxswing.service] package implements the synchronous (IService, Service) and asynchronous (IRxService, RxService) service layers;
  • the [rxswing.ui] package implements the Swing interface;

8.3. Running the project

To run the project in IntelliJ IDEA, follow these steps:

 

8.4. The synchronous service

Image

  

The synchronous service layer provides the following [IService] interface:


package dvp.rxswing.service;
 
public interface IService {
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of milliseconds,
  // where [delay] is itself a random number in the interval [minDelay, maxDelay]
  public ServiceResponse getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

The [ServiceResponse] type of the service response is as follows:


package dvp.rxswing.service;
 
import java.util.List;
 
public class ServiceResponse {
 
  // service waiting time
  private int delay;
  // random numbers
  private List<Integer> aleas;
  // execution thread
  private String executedOn;
 
  // manufacturers
 
  public ServiceResponse() {
      // execution thread
    executedOn = Thread.currentThread().getName();
  }
 
  public ServiceResponse(int delay, List<Integer> aleas) {
      // local builder
    this();
    // other initializations
    this.delay = delay;
    this.aleas = aleas;
  }
 
  // getters and setters
...
}

The [IService] interface is implemented by the following [Service] class:


package dvp.rxswing.service;
 
import java.util.*;

public class Service implements IService {
 
  @Override
  public ServiceResponse getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    // random numbers in the [a,b] interval
    // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
    // numbers are generated after a delay of milliseconds,
    // where [delay] is itself a random number in the interval [minDelay, maxDelay]
 
    // some checks
    List<String> messages = new ArrayList<>();
    int erreur = 0;
    if (a < 0) {
      messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
      erreur |= 2;
    }
    if (a >= b) {
      messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
      erreur |= 4;
    }
    if (minCount < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
      erreur |= 16;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
      erreur |= 32;
    }
    if (minDelay < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
      erreur |= 64;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
      erreur |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
      erreur |= 256;
    }
    // mistakes?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // random number generator
    Random random = new Random();
    // waiting?
    int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
    if (delay > 0) {
      try {
        Thread.sleep(delay);
      } catch (InterruptedException e) {
        throw new AleasException(String.format("[%s : %s]", e.getClass().getName(), e.getMessage()), 1024);
      }
    }
    // result generation
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> nombres = new ArrayList<>();
    for (int i = 0; i < count; i++) {
      nombres.add(a + random.nextInt(b - a + 1));
    }
    // return result
    return new ServiceResponse(delay,nombres);
  }
 
}

The exception class [AleasException] used by the service is as follows:


package dvp.rxswing.service;
 
public class AleasException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
    // error code
  private int code;
 
  // manufacturers
  public AleasException() {
  }
 
  public AleasException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }
 
  public AleasException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }
 
  public AleasException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }
 
  // getters and setters
...
}
  • line 3: it extends the [RuntimeException] class. It is therefore an unhandled exception;
  • line 7: it adds an error code to its parent class (0=no error);

8.5. The asynchronous service

Image

  

The asynchronous service layer provides the following [IRxService] interface:


package dvp.rxswing.service;
 
import dvp.rxswing.ui.UiResponse;
import rx.Observable;
 
public interface IRxService {
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of milliseconds,
  // where [delay] is itself a random number in the interval [minDelay, maxDelay]
  public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse);
}
  • line 11: the [getAleas] method of the service now returns an observable;

The [getAleas] method returns a response of type [UiResponse] intended for the [Ui] layer. This type is as follows:


package dvp.rxswing.ui;
 
import dvp.rxswing.service.ServiceResponse;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
 
public class UiResponse {
 
  // customer id
  private int idClient;
  // service response
  private ServiceResponse serviceResponse;
  // observation thread name
  private String observedOn;
  // query time
  private String requestAt;
  // response time
  private String responseAt;
 
  // manufacturers
 
  public UiResponse() {
      // observation thread
    observedOn = Thread.currentThread().getName();
    // query time
    requestAt = getTimeStamp();
  }
 
  // private methods
 
  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }
 
  // getters and setters
...
}
  • The random numbers are in the field on line 13;
  • the other fields are there to specify the execution and observation threads of the asynchronous service's observable, as well as the timestamps of the request made to the service and the response received;

The asynchronous interface is implemented by the following [RxService] class:


package dvp.rxswing.service;
 
import dvp.rxswing.ui.UiResponse;
import rx.Observable;
 
public class RxService implements IRxService {
 
  // synchronous service
  private IService service;
 
  // manufacturer
  public RxService(IService service) {
    this.service = service;
  }
 
  @Override
  public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse) {
      // we create an observable emitting the value rendered by the synchronous service
    return Observable.create(subscriber -> {
      try {
          // synchronous call
        uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        // the result is passed on to the observer
        subscriber.onNext(uiResponse);
      } catch (Exception e) {
          // we pass the error to the observer
        subscriber.onError(e);
      } finally {
          // the observer is informed that the emissions are finished
        subscriber.onCompleted();
      }
    });
  }
}
  • lines 12–14: the [RxService] class of the asynchronous service is constructed from an instance of the synchronous interface [IService];
  • lines 20–33: construction of the observable, the result of the [getAleas] method;
  • line 22: the synchronous method [service.getAleas] is called. Its result of type [ServiceResponse] is included in the object of type [UiResponse] to be provided to the [swing] layer. This object was initially passed in the method’s call parameters (last parameter, line 17);
  • line 24: the [UiResponse] is sent to the observer (the [swing] layer). The [UiResponse] object contains not only the information generated by the synchronous service on line 22, but also other information generated by the method calling the [getAleas] method on line 17. This is why the calling method passed the [UiResponse] object as a parameter to the [getAleas] method (last parameter, line 17);
  • line 30: we don’t forget to signal the end of the emissions. Here we have an observable emitting only one value: the one returned by the synchronous service;
  • line 27: we notify the observer of any errors;

8.6. The graphical user interface

Image

  
  • The graphical user interface was built using the [NetBeans] IDE, which has a good graphical editor. This editor generated the [AbstractJFrameAleas.form] file, which can only be used by this IDE;
  • The [AbstractJFrameAleas] class was also generated by the NetBeans graphical editor. It was then refactored as follows: the GUI events we wanted to handle are processed in the [AbstractJFrameAleas] class via abstract methods implemented in the child class [JFrameAleasEvents]. Ultimately,
    • the abstract class [AbstractJFrameAleas] is responsible for building and displaying the GUI;
    • the child class [JFrameAleasEvents] handles its events;

The GUI components of the [Request] tab are as follows:

 
No.
type
name
role
1
JTabbedPane
jTabbedPane1
A tabbed container. Contains two tabs (JPanel): [jPanelRequest] for the request and [jPanelResponse] for the response;
2
JTextField
jTextFieldNbValues
the number of requests to be made to the random number service. In the case of the asynchronous service running on the scheduler [Schedulers.io], these requests will share a processor;
3
JTextField
jTextFieldA
endpoint a of the interval [a,b]
4
JTextField
jTextFieldB
endpoint b of the interval [a,b]
5
JTextField
jTextFieldMinCount
minCount endpoint of the interval [minCount, maxCount]
6
JTextField
jTextFieldMaxCount
maxCount bound of the interval [minCount, maxCount]
7
JTextField
jTextFieldMinDelay
minDelay bound of the interval [minDelay, maxDelay]
8
JTextField
jTextFieldMaxDelay
maxDelay bound of the interval [minDelay, maxDelay]
9
JCheckBox
jCheckBoxRxSwing
If the checkbox is checked, requests are made to the asynchronous interface. Otherwise, they are made to the synchronous interface
10
JComboBox
jComboBoxSchedulers
For asynchronous requests, they will be executed using the scheduler selected here
11
JButton
jButtonGenerate
launches the execution of requests to the synchronous or asynchronous service

The GUI components of the [Response] tab are as follows:

 
No.
type
name
role
1
JLabel
jLabelDuration
the total execution time in milliseconds of the requests
2
JLabel
jLabelNbResponses
the total number of observed responses (may differ from the number of requests, as each request may return multiple values to be observed)
3
JList
jListNumbers
display of observed (received) values
4
JButton
jButtonCancel
cancels requests currently being executed

8.7. Instantiation of the graphical user interface

  

The [JFrameAleasEvents] class handles GUI events, including clicks on the [Generate] button. It is an executable class that runs in the following context:


public class JFrameAleasEvents extends AbstractJFrameAleas {
 
    private static final long serialVersionUID = 1L;
    // synchronous generation service
    private IService service;
    // asynchronous generation service
    private IRxService rxService;
 
    // seizures
    private int nbRequests;
    private int a;
    private int b;
    private int minDelay;
    private int maxDelay;
    private int minCount;
    private int maxCount;
 
    // error messages
    private final String jLabelNbValuesErrorText = "Tapez un nombre entier >=1";
    private final String jLabelCountErrorText = "minCount doit être >=0 et maxCount>=minCount ";
    private final String jLabelDelayErrorText = "minDelay doit être >=0 et maxDelay>=minDelay et  maxDelay<=5000";
    private final String jLabelIntervalErrorText = "a doit être >=0 et b>=a ";
 
    // subscriptions to observables
    protected List<Subscription> subscriptions = new ArrayList<Subscription>();
    // start-end of execution
    private long debut;
    // mapper jSON
    private ObjectMapper jsonMapper;
    // answer model
    private DefaultListModel<String> model;
 
    // manufacturer
    public JFrameAleasEvents() {
        // parent
        super();
        // local
        initJFrame();
        // services
        service = new Service();
        rxService = new RxService(service);
        // mapper jSON
        jsonMapper = new ObjectMapper();
    }
 
    private void initJFrame() {
        // hide error messages
        jLabelCountError.setText("");
        jLabelDelayError.setText("");
        jLabelIntervalError.setText("");
        jLabelNbValuesError.setText("");
        // hide texts by default
        jTextFieldA.setText("100");
        jTextFieldB.setText("200");
        jTextFieldMinCount.setText("5");
        jTextFieldMaxCount.setText("10");
        jTextFieldMinDelay.setText("100");
        jTextFieldMaxDelay.setText("500");
        jTextFieldNbValeurs.setText("10");
        jLabelDuree.setText("");
        // answer model
        model = new DefaultListModel<>();
        jListNumbers.setModel(model);
        // number of hearts
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
    }
 
    public static void main(String args[]) {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException
                | IllegalAccessException e) {
            System.out.println(e);
            System.exit(0);
        }
 
        /* Create and display the form */
        java.awt.EventQueue.invokeLater(() -> {
            new JFrameAleasEvents().setVisible(true);
        });
    }
  • line 1: the [JFrameAleasEvents] class extends the [AbstractJFrameAleas] class, which itself extends the Swing [JFrame] class. The [JFrameAleasEvents] class is therefore a Swing window;
  • lines 68–75: the [main] method that will be executed;
  • line 70: sets the look and feel of the GUI;
  • line 79: the constructor of the [JFrameAleasEvents] class is called: the GUI will be constructed and initialized. Once this is done, it is made visible;
  • lines 34–44: the constructor;
  • line 36: the call to the parent constructor will initialize the GUI. At this point, it looks exactly as the developer designed it. It is not yet visible;
  • line 38: certain components of the GUI are initialized;
  • line 40: instantiation of the synchronous service;
  • line 41: instantiation of the asynchronous service;

8.8. Execution of synchronous requests

Clicking the [Generate] button triggers the execution of the following [doGenerate] method:


    @Override
    protected void doGenerate() {
        // saisies valides ?
        if (!isPageValid()) {
            return;
        }
        // rx ou pas ?
        if (jCheckBoxRxSwing.isSelected()) {
            // requêtes asynchrones
            doGenerateWithRxService();
        } else {
            // requêtes synchrones
            doGenerateWithService();
        }
}
  • lines 4–6: we verify that the user’s input is valid. We won’t comment on the [isPageValid] method. It’s basic;
  • line 8: we check the state of the RxSwing checkbox;
  • line 13: we execute the requests synchronously;

The [doGenerateWithService] method is as follows:


    // synchronous generation
    private void doGenerateWithService() {
        // start waiting
        beginWaiting();
        try {
            for (int i = 0; i < nbRequests; i++) {
                // response preparation
                UiResponse uiResponse = new UiResponse();
                // customer no
                uiResponse.setIdClient(i);
                // synchronous call
                uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
                // response time
                uiResponse.setResponseAt();
                // update the JList model with the responses received
                model.add(0, jsonMapper.writeValueAsString(uiResponse));
                // update number of responses
                jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
            }
        } catch (JsonProcessingException | RuntimeException e) {
            JOptionPane.showMessageDialog(this, getInfoForThrowable("L'erreur suivante s'est produite", e), "Informations",
                    JOptionPane.PLAIN_MESSAGE);
        }
        // end waiting
        endWaiting();
}
  • line 12: synchronous call to the random number generation service;
  • the [doGenerateWithService] method is executed entirely within the Swing event loop thread. Until the method is finished, the GUI does not process any new events. It is frozen. Thus, for example, the GUI updates in lines 16 and 18 will never be seen. They will only be visible with their final values, and this will occur at the end of the execution of all requests;

The [beginWaiting] method (line 4) is as follows:


    private void beginWaiting() {
        // buttons
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // wait slider
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // raz answers
        model.clear();
        // rx subscriptions
        subscriptions.clear();
        // the response view is displayed
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // start of execution
        debut = new Date().getTime();
}
  • line 3: the [Generate] button is hidden. This creates an event that can also only be executed once all requests have finished running. We never see it hidden, however, because the [endWaiting] method on line 25 of the [doGenerateWithService] method displays it again;
  • line 13: we select the [Response] tab to see the responses arrive. Again, this event will only be executed after all requests have finished, at which point we will see all the responses at once, whereas we wanted to see them arrive one after another;

The synchronous interface clearly has shortcomings. These are overcome by the asynchronous interface.

8.9. Executing Asynchronous Requests

The code for executing asynchronous requests is as follows:


private void doGenerateWithRxService() {
        // début attente
        beginWaiting();
        // on va obtenir les nombres aléatoires sous la forme d'un observable
        Observable<UiResponse> observable = Observable.empty();
        // Schéduler d'exécution des différents observables
        Scheduler[] schedulers = { Schedulers.io(), Schedulers.computation(), Schedulers.newThread(),
                Schedulers.trampoline(), Schedulers.immediate() };
        Scheduler scheduler = schedulers[jComboBoxSchedulers.getSelectedIndex()];
        // configuration des observables
        for (int i = 0; i < nbRequests; i++) {
            // préparation réponse
            UiResponse uiResponse = new UiResponse();
            uiResponse.setIdClient(i);
            // l'observable est configuré pour s'exécuter sur le schéduler choisi par l'utilisateur
            // puis cumul de l'observable obtenu à l'observable du tout
            observable = observable.mergeWith(
                    rxService.getAleas(a, b, minCount, maxCount, minDelay, maxDelay, uiResponse).subscribeOn(scheduler));
        }
        // observateur
        observable = observable.observeOn(SwingScheduler.getInstance());
        // pour l'instant, on a juste fait de la configuration
        // aucune requête n'a encore été faite au service synchrone de génération des nombres aléatoires
        // on s'abonne à l'observable - c'est ce qui va provoquer l'appel au service synchrone de génération des nombres aléatoires
        try {
            // il n'y a ici qu'un abonnement - le résultat est une souscription
            subscriptions.add(observable.subscribe(
                    // notification d'émission
                    uiResponse -> {
                        // on met à jour l'Ui avec la réponse
                        // ceci est possible car l'observation a lieu dans le thread de l'Ui
                        updateUi(uiResponse);
                    } ,
                    // notification d'erreur
                    th -> {
                        // cas d'erreur - on l'affiche
                        String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
                        JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
                        // annulation requêtes
                        doCancel();
                    } ,
                    // notification [onCompleted]
                    // fin de l'attente
                    this::endWaiting));
        } catch (Throwable th) {
            // cas d'exception + générale - on l'affiche
            String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
            JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
            // on annule les requêtes
            doCancel();
        }
    }
  • line 3: the GUI is updated to indicate that a potentially long-running operation is in progress;
  • line 5: an empty observable is created. This observable will be observed by the [Swing] layer;
  • line 7: the array of possible schedulers;
  • line 9: we have given the user the ability to choose the scheduler on which to execute the queries. We retrieve the scheduler of their choice;
  • lines 11–19: each request returns an observable whose elements are merged (mergeWith) (line 17) into the observable from line 5;
  • lines 13–14: the [UiResponse] object is constructed. Recall that this object is both the input parameter of the [RxService.getAleas] method and its result (lines 17–18);
  • Line 14: Each request is identified by a number, referred to here as [idClient]. This is necessary because, in an asynchronous environment, the order in which responses are received may differ from the order in which requests were sent. [idClient] allows us to determine which request the response belongs to;
  • Lines 17–18: The asynchronous request is made [rxService.getRandom]. It is executed on the scheduler chosen by the user. Its result, of type Observable<UiResponse>, is combined with the observable from line 5. It is important to note that the method [rxService.getAleas] is executed here and returns an observable. However, this does not mean that random numbers have been generated. In fact, an observable is only executed when subscribed to. This is not yet the case;
  • line 21: this is the key instruction: we specify that the elements emitted by the observable on line 5 should be observed on the UI thread. Here, we use a scheduler specific to the RxSwing library;
  • lines 25–51: we subscribe to the observable from line 5. Only now will the random numbers be requested from the synchronous service that generates them. The key part is in the instructions on lines 29–33. The rest primarily handles error cases and the [onCompleted] notification from the observable;
  • lines 28–44: Remember that we requested to observe the process from line 5 on the UI thread. Therefore, the code in lines 28–44 runs on the UI thread;
  • lines 29–33: We handle the observable’s [onNext] notification. We receive a [UiResponse] type emitted by the observed process. This is the result of one of the asynchronous requests. We update the user interface with this response;
  • lines 34–41: we handle the observable’s [onError] notification. We display a dialog box showing the error (lines 37–38) and then cancel the requests (line 40);
  • lines 42–44: we handle the observable’s [onCompleted] notification. We update the GUI to show that the requested service has completed. Line 44 could also have been written as follows
 ()->{endWaiting();}

Here, we chose to use a method reference;

  • lines 45–51: certain exceptions do not go through lines 34–41. This happens when too many requests are made. Once a certain limit—which depends on the runtime environment—is exceeded, a [StackOverflowError] occurs, which is handled by lines 45–51;
  • Line 27: The subscription produces a [Subscription] type that is added to a list of subscriptions. This list will have only one element here;

Line 32: We update the user interface using the following [updateUi] method:


    private void updateUi(UiResponse uiResponse) {
        // response time
        uiResponse.setResponseAt();
        // observation thread
        uiResponse.setObservedOn();
        // number of responses
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // running time
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // add the jSON response string to the JList response template
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Here we see that components of the graphical user interface are updated (lines 7, 9, 12). For this to be possible, you must be in the UI thread (event loop).

The [endWaiting] method is as follows:


    private void endWaiting() {
        // generate] button visible
        jButtonGenerate.setVisible(true);
        // hidden [Cancel] button
        jButtonCancel.setVisible(false);
        // hidden wait cursor
        jTabbedPane1.setCursor(Cursor.getDefaultCursor());
        // selected answers tab
        jTabbedPane1.setSelectedIndex(1);
        // duration updated one last time
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
}

The [doCancel] method is called when an error occurs during the execution of asynchronous requests or when the user clicks the [Cancel] button. Its code is as follows:


// les souscriptions aux observables
    private List<Subscription> subscriptions = new ArrayList<Subscription>();
....
 
    @Override
    protected void doCancel() {
        // fin attente
        endWaiting();
        // dans le cas de souscriptions
        if (jCheckBoxRxSwing.isSelected() && subscriptions != null) {
            subscriptions.forEach(Subscription::unsubscribe);
            //subscriptions.forEach(s -> s.unsubscribe());
        }
    }
 
  • line 2: [subscriptions] is a list of subscriptions;
  • line 11: all subscriptions are canceled;
  • line 12: another way of writing line 11. The [forEach] method expects an instance of type Consumer<Subscription> here (see section 4.4);

Let’s return to the code for the [doGenerateWithService] method: it can be broken down into two steps:

  1. the observable configuration step. This is done in the thread of the caller of the [doGenerateWithService] method, i.e., the UI thread;
  2. the subscription that will trigger the execution of the observables;

If the observables use one of the schedulers [Schedulers.computation(), Scheduler.io(), Schedulers.newThread()], then they will execute outside the UI thread. These different threads will compete for the machine’s core(s). Since the requests are long-running operations (several hundred milliseconds), the [doGenerateWithService] method executed in the UI thread will finish before the requests have returned their responses. However, this method had been executed on the click event of the [Generate] button. Once this event is processed, the UI thread will be able to move on to processing the following events. There are several of them. Thus, the [beginWaiting] method had set up several:


    private void beginWaiting() {
        // buttons
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // waiting cursor
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // raz answers
        model.clear();
        // rx subscriptions
        subscriptions.clear();
        // the response view is displayed
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // start of execution
        debut = new Date().getTime();
}

Virtually every line of this code affects the graphical user interface. This update does not occur immediately: events are placed in the event loop queue. Once the click event on the [Generate] button is processed, these events are executed in turn, and the user can see the graphical user interface change:

  • the [Response] tab is displayed (line 13) and a loading indicator is associated with it (line 6)
  • its [Cancel] button is displayed (line 4) and the user can click on it;
  • the JList of responses is cleared (line 9);
  • the JLabel for the number of responses displays 0;
  • the JLabel for the execution time displays an empty string;

Throughout the execution of the queries, the UI thread has regular access to the processor. It can then process pending events. Among these are those set by the [updateUi] method:


    private void updateUi(UiResponse uiResponse) {
        // response time
        uiResponse.setResponseAt();
        // observation thread
        uiResponse.setObservedOn();
        // number of responses
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // running time
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // add the jSON response string to the JList response template
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

When the UI thread is active:

  • the JLabel displaying the number of responses is updated (line 7);
  • the JLabel for the execution time is updated (line 9);
  • the JList of responses is updated via its model (line 12);

This allows the user to see the progress of the query execution. Additionally, they can cancel them via the [Cancel] button. This is the whole point of having asynchronous services in front of the [Swing] layer, and RxJava is the technology of choice for implementing them.

Finally, note that if the user chooses one of the schedulers [Schedulers.immediate(), Schedulers.trampoline()], the observables are then executed on the same thread as the caller, i.e., the UI thread. This results in synchronous behavior.

The results obtained with the different schedulers were shown in sections 2.8.1, 2.8.2, 2.8.3, and 2.8.4.