Especialização em Sistemas com Java 21 LTS • Capítulo 18


🎯 Visão Geral dos Exemplos Práticos

Neste laboratório prático, implementaremos exemplos corporativos completos explorando:

  1. Aplicação Desktop Reativa em JavaFX 21: implementação de interface fluida de checkout de PDV com TableView e ObservableList.
  2. Sincronização com Data Bindings: cálculos de totais, descontos e habilitação de botões disparados automaticamente pelo grafo de propriedades.
  3. Despacho Seguro com Platform.runLater: atualização de métricas da interface a partir de threads concorrentes de segundo plano.

💻 Exemplo 01 — Terminal PDV Completo com JavaFX e Data Binding

package com.empresa.ui;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;

import java.text.NumberFormat;
import java.util.Locale;

/**
 * Terminal de Ponto de Venda comercial com JavaFX puro e reatividade com ObservableProperties.
 */
public class TerminalPdvReativoApp extends Application {

    public record ItemCarrinho(String codigo, String descricao, int quantidade, double precoUnitario) {
        public double subtotal() { return quantidade * precoUnitario; }
    }

    private final ObservableList<ItemCarrinho> itensVenda = FXCollections.observableArrayList();
    private final DoubleProperty totalBrutoProperty = new SimpleDoubleProperty(0.0);

    @Override
    public void start(Stage palcoPrincipal) {
        palcoPrincipal.setTitle("Terminal de Vendas PDV (JavaFX 21 LTS)");

        // 1. Título e Cabeçalho do Caixa
        Label lblHeader = new Label("SISTEMA DE PONTO DE VENDA — TERMINAL #04");
        lblHeader.setStyle("-fx-font-size: 16px; -fx-font-weight: bold; -fx-text-fill: #0d47a1;");

        // 2. Tabela de Itens Reativa (TableView com ObservableList)
        TableView<ItemCarrinho> gridItens = new TableView<>(itensVenda);
        gridItens.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);

        TableColumn<ItemCarrinho, String> colCod = new TableColumn<>("CÓDIGO");
        colCod.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(f.getValue().codigo()));

        TableColumn<ItemCarrinho, String> colDesc = new TableColumn<>("PRODUTO");
        colDesc.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(f.getValue().descricao()));

        TableColumn<ItemCarrinho, String> colQtd = new TableColumn<>("QTD");
        colQtd.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(String.valueOf(f.getValue().quantidade())));

        TableColumn<ItemCarrinho, String> colPreco = new TableColumn<>("UNITÁRIO");
        colPreco.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(formatarBRL(f.getValue().precoUnitario())));

        TableColumn<ItemCarrinho, String> colSub = new TableColumn<>("SUBTOTAL");
        colSub.setCellValueFactory(f -> new javafx.beans.property.SimpleStringProperty(formatarBRL(f.getValue().subtotal())));

        gridItens.getColumns().addAll(colCod, colDesc, colQtd, colPreco, colSub);

        // 3. Formulário de Inclusão Rápida de Produtos
        TextField txtCod = new TextField();
        txtCod.setPromptText("SKU / Código");
        TextField txtDesc = new TextField();
        txtDesc.setPromptText("Descrição da mercadoria");
        TextField txtQtd = new TextField("1");
        txtQtd.setPrefWidth(60);
        TextField txtPreco = new TextField();
        txtPreco.setPromptText("Preço R$");

        Button btnAdicionar = new Button("Inserir (Enter)");
        btnAdicionar.setStyle("-fx-background-color: #1b5e20; -fx-text-fill: white; -fx-font-weight: bold;");

        btnAdicionar.setOnAction(e -> {
            try {
                String cod = txtCod.getText().trim();
                String desc = txtDesc.getText().trim();
                int qtd = Integer.parseInt(txtQtd.getText().trim());
                double preco = Double.parseDouble(txtPreco.getText().trim().replace(',', '.'));

                if (!cod.isEmpty() && !desc.isEmpty() && qtd > 0 && preco > 0) {
                    ItemCarrinho novo = new ItemCarrinho(cod, desc, qtd, preco);
                    itensVenda.add(novo);
                    totalBrutoProperty.set(totalBrutoProperty.get() + novo.subtotal());

                    txtCod.clear();
                    txtDesc.clear();
                    txtQtd.setText("1");
                    txtPreco.clear();
                    txtCod.requestFocus();
                }
            } catch (NumberFormatException ex) {
                new Alert(Alert.AlertType.WARNING, "Informe valores numéricos válidos para quantidade e preço!").showAndWait();
            }
        });

        HBox painelEntrada = new HBox(10, txtCod, txtDesc, txtQtd, txtPreco, btnAdicionar);
        painelEntrada.setAlignment(Pos.CENTER_LEFT);

        // 4. Rodapé Financeiro com Data Bindings
        Label lblTotal = new Label();
        lblTotal.setStyle("-fx-font-size: 22px; -fx-font-weight: bold; -fx-text-fill: #b71c1c;");
        lblTotal.textProperty().bind(Bindings.createStringBinding(
                () -> "TOTAL A PAGAR: " + formatarBRL(totalBrutoProperty.get()),
                totalBrutoProperty
        ));

        Button btnFecharCaixa = new Button("💳 Finalizar Venda");
        btnFecharCaixa.setStyle("-fx-background-color: #0d47a1; -fx-text-fill: white; -fx-font-size: 14px; -fx-font-weight: bold;");
        btnFecharCaixa.disableProperty().bind(Bindings.isEmpty(itensVenda));

        btnFecharCaixa.setOnAction(e -> {
            new Alert(Alert.AlertType.INFORMATION, "Cupom Fiscal emitido com sucesso! " + lblTotal.getText()).showAndWait();
            itensVenda.clear();
            totalBrutoProperty.set(0.0);
        });

        HBox painelRodape = new HBox(25, lblTotal, btnFecharCaixa);
        painelRodape.setAlignment(Pos.CENTER_RIGHT);

        // Estrutura Principal de Layout
        VBox layoutRaiz = new VBox(15, lblHeader, painelEntrada, gridItens, painelRodape);
        layoutRaiz.setPadding(new Insets(15));
        VBox.setVgrow(gridItens, Priority.ALWAYS);

        Scene cena = new Scene(layoutRaiz, 820, 520);
        palcoPrincipal.setScene(cena);
        palcoPrincipal.show();
    }

    private static String formatarBRL(double valor) {
        NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.of("pt", "BR"));
        return nf.format(valor);
    }

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

💻 Exemplo 02 — Monitoramento Assíncrono com Platform.runLater

package com.empresa.ui;

import javafx.application.Platform;
import javafx.beans.property.StringProperty;

/**
 * Padrão seguro para atualização de elementos gráficos do JavaFX a partir de threads concorrentes.
 */
public class AtualizadorSeguroUi {

    public static void simularColetaTelemetriaAssincrona(StringProperty targetProperty) {
        // Dispara uma thread virtual em background para coletar dados de sensores de rede
        Thread.ofVirtual().name("telemetria-sensor").start(() -> {
            try {
                for (int ciclo = 1; ciclo <= 5; ciclo++) {
                    Thread.sleep(1000); // Coleta a cada 1 segundo
                    final String leitura = String.format("📡 Telemetria Ciclo #%d: Temperatura 48.%d°C | Carga: %d%%",
                            ciclo, ciclo * 2, 20 + ciclo * 5);

                    // IMPORTANTE: Nunca toque na UI diretamente da thread em segundo plano.
                    // Use Platform.runLater para despachar para a JavaFX Application Thread:
                    Platform.runLater(() -> {
                        targetProperty.set(leitura);
                        System.out.println("   [UI ATUALIZADA]: " + leitura);
                    });
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("================================================================================");
        System.out.println("         ATUALIZAÇÃO SEGURA DA JAVAFX APPLICATION THREAD COM RUNLATER           ");
        System.out.println("================================================================================");
        System.out.println("Simulação do despacho de telemetria assíncrona para a thread gráfica...");
        System.out.println("A interface gráfica JavaFX garante execução thread-safe via Platform.runLater().");
        System.out.println("================================================================================");
    }
}

📋 Resultado Esperado no Console

================================================================================
         ATUALIZAÇÃO SEGURA DA JAVAFX APPLICATION THREAD COM RUNLATER           
================================================================================
Simulação do despacho de telemetria assíncrona para a thread gráfica...
A interface gráfica JavaFX garante execução thread-safe via Platform.runLater().
   [UI ATUALIZADA]: 📡 Telemetria Ciclo #1: Temperatura 48.2°C | Carga: 25%
   [UI ATUALIZADA]: 📡 Telemetria Ciclo #2: Temperatura 48.4°C | Carga: 30%
   [UI ATUALIZADA]: 📡 Telemetria Ciclo #3: Temperatura 48.6°C | Carga: 35%
   [UI ATUALIZADA]: 📡 Telemetria Ciclo #4: Temperatura 48.8°C | Carga: 40%
   [UI ATUALIZADA]: 📡 Telemetria Ciclo #5: Temperatura 48.10°C | Carga: 45%
================================================================================