TSM - Concurență şi data binding în JavaFX

Silviu Dumitrescu - Line manager@Telenav


În articolul din acest număr vă aducem din nou în atenție provocări tehnologice din lumea JavaFX. În articolul al doilea vom discuta despre concurență şi data binding.

Pachetul javafx.concurrent gestionează codul multifir al interacțiunii cu UI-ul şi asigură că această interacțiune are loc în firul corect. Pachetul constă din interfața Worker şi două clase de bază Task şi Service, ambele implementând interfața Worker.

Interfața Woker furnizează API-ul folosit de un "background worker" ce comunică cu UI-ul. Clasa Task este o implementare complet observabilă a clasei java.util.concurrent.FutureTask şi permite dezvoltatorilor să implementeze task-uri asincrone în aplicațiile JavaFX. Clasa Service execută aceste task-uri.

Un Worker este asadar un obiect ce lucrează într-un fir din background. Starea obiectului Worker este observabilă și utilizabilă din firele aplicației JavaFX.

Ciclul de viață al lui Worker este definit astfel: când este creat obiectul Worker, acesta este în starea READY. După ce a fost programat pentru lucru, obiectul Worker tranzitează către starea SCHEDULED. După aceea, când obiectul Worker rulează, starea sa devine RUNNING.

Observație: chiar dacă obiectul Worker a pornit imediat, fără a fi programat, el tranzitează mai întâi în starea SCHEDULED şi apoi în RUNNING.

Starea obiectului Worker, atunci când se execută cu succes devine SUCCEEDED, iar proprietatea valoare va fi setată la rezultatul obiectului Worker. Altfel, dacă sunt aruncate excepții pe timpul execuției obiectului Worker, starea sa devine FAILED, iar proprietatea excepție este setată la tipul de excepție apărut. În orice stare obiectul Worker poate fi întrerupt utilizând metoda cancel(), ceea ce trimite obiectul în starea CANCELLED.

Progresul înregistrat la rularea obiectului Worker poate fi obținut prin trei proprietăți diferite: totalWork, workDone şi progress.

Clasa Task poate fi pornită în unul dintre următoarele moduri (primele două ar fi preferabile):

Taskurile sunt utilizate pentru a implementa logica de lucru într-un fir din background. Pentru început trebuie să extindem clasa Task, care va suprascrie metoda call(). Clasa Task moștenește clasa java.utils.concurrent.FutureTask, ce implementează interfața Runnable. De aceea obiectul Task poate fi utilizat cu API-ul Executor şi poate fi trimis unui fir ca parametru.

Putem apela obiectul Task direct prin FutureTask.run(), ceea ce ne permite să apelăm acest task dintr-un alt fir.

Vom crea o clasă CounterTask ce extinde clasa Task.

public class CounterTask extends Task {
    @Override
    public Void call() {
        final int max = 10000000;
        updateProgress(0, max);
        for (int i = 1; i <= max; i++) {
            updateProgress(i, max);
        }
        return null;
    }
}

Metoda call() este invocată de firul din background, de aceea această metodă poate manipula stări ce sunt sigure a fi citite sau scrise dintr-un fir din background. Spre exemplu, manipularea scenei grafice active din metoda call() va arunca o runtime exception.

Pe de altă parte, clasa Task este destinată a fi utilizată cu aplicații JavaFX şi ne asigură că orice modificări ale proprietăților publice, notificări de eroare, manipulatoare de evenimente şi stări apar în firul aplicației JavaFX. În interiorul metodei call() putem utiliza metodele: updateProgress(), updateMessage() şi updateTitle() pentru a actualiza valorile corespunzătoare proprietăților pe firul JavaFX. În aplicație am creat o instanță a clasei anterioare, numită countTask şi am executat-o printr-un ExecutorService (ExecutorService es = Executors.newSingleThreadExecutor();):

@Override
public void handle(ActionEvent event) {
    System.out.println("Count Started");
    bar.progressProperty().bind(countTask.progressProperty());
    es.execute(countTask);
}

Clasa Service este destinată executării unui obiect Task dintr-unul sau mai multe fire. Metodele şi stările clasei Service trebuie accesate din firul aplicației JavaFX. Această clasă ajută dezvoltatorii să implementeze o interacțiune corectă între firele din background şi firul aplicației JavaFX. Putem porni, opri, anula şi restarta un Service. Un Service poate rula un task mai mult de o dată. Așadar, un serviciu poate fi definit declarativ şi restartat la cerere.

Un Service poate fi executat în unul dintre următoarele modalități:

Exemplu de creare a unui service custom este dat în exemplul de mai jos:

public class CounterService extends Service {
    @Override
    protected Task createTask() {
        CounterTask ct = new CounterTask();
        return ct;
    }
}

Creăm în aplicația JavaFX un CounterService (CounterService cs = new CounterService();) şi pornim firul astfel:

if (cs.getState() == State.READY) {
    cs.start();
}

Data Binding

Data binding-ul are rolul de a simplifica task-ul sincronizând view-ul cu datele din model. Legarea (binding-ul) observă listele sale de dependențe pentru a detecta schimbări şi se actualizează dacă acestea au apărut. API-ul de binding furnizează un mod simplu de a crea legări pentru cele mai comune situații.

Binding-ul este așadar un mecanism puternic pentru exprimarea relațiilor directe între variabile. Când obiectele participă la legări, modificările efectuate unuia vor fi automat reflectate celuilalt. Spre exemplu, binding-ul poate fi utilizat în GUI pentru păstrarea automată a afișărilor sincronizate cu datele pe care le referă.

Binding-urile sunt asamblate din una sau mai multe surse numite dependențe.

În exemplul nostru anterior am folosit funcția bind() pentru a lega progress bar-ul de counterTask. Iată codul complet al aplicației JavaFX:

public class CounterBarAppService extends Application {
    StackPane root = new StackPane();
    VBox mainBox = new VBox();
    ProgressBar bar = new ProgressBar(0.0);
    CounterService cs = new CounterService();

    @Override
    public void init() throws Exception {
        super.init();

        mainBox.setAlignment(Pos.CENTER);
        mainBox.setSpacing(10);

        Button btn = new Button();
        btn.setText("Count to Ten Million!");
        btn.setOnAction(new EventHandler() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Count Started");
                bar.progressProperty().bind(cs.progressProperty());
                if (cs.getState() == State.READY) {
                    cs.start();
                }
            }
        });

        Button restartBtn = new Button();
        restartBtn.setText("Restart");
        restartBtn.setOnAction(new EventHandler() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Count Started");
                bar.progressProperty().bind(cs.progressProperty());
                cs.restart();
            }
        });

        mainBox.getChildren().add(btn);
        mainBox.getChildren().add(restartBtn);
        mainBox.getChildren().add(bar);
        root.getChildren().add(mainBox);
    }

    @Override
    public void stop() throws Exception {
        super.stop();
    }

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("JavaFX Service Example");

        primaryStage.setScene(new Scene(root, 400, 250));
        primaryStage.show();
    }
}

Vă așteptăm cu mare plăcere la discuții despre noua lume JavaFX.

Lectură plăcută!