📚 Pré-requisitos: É altamente recomendado ter concluído a Parte 01 — API REST e a Parte 03 — Cliente Desktop com Java Swing. Ter a API Spring Boot em execução na porta
8080.
Bem-vindo à Parte 09 da Trilha Prática de Aplicações Full Stack!
Na Parte 03, você construiu um cliente desktop funcional utilizando a biblioteca clássica Java Swing. Aquele projeto cumpriu perfeitamente o papel de introduzir o consumo de APIs REST no desktop sem a sobrecarga de sistemas modulares.
Agora, na Parte 09, você dará o salto profissional definitivo no ecossistema desktop em Java: aprenderá como e por que a indústria migrou do Swing para o JavaFX moderno, adotando o padrão MVC (Model-View-Controller), interfaces declarativas em FXML, estilização com CSS e propriedades reativas (ObservableList).
Trilha de Evolução do Cliente Desktop:
├── Parte 03: Java Swing (Desktop Clássico) ← UI Imperativa em código Java puro
└── Parte 09: JavaFX 21 Moderno (Evolução Arquitetural) ← FXML Declarativo + CSS + MVC + Reatividade
Compreender a evolução entre Swing e JavaFX é uma das competências mais valorizadas na engenharia de software para sistemas legados e modernos em Java:
| Dimensão Arquitetural | Como era no Swing (Parte 03) | Como é no JavaFX Moderno (Parte 09) | Benefício de Engenharia |
|---|---|---|---|
| Construção da Interface | Imperativa: Componentes instanciados e posicionados manualmente em código Java (panel.add(button, BorderLayout.EAST)). |
Declarativa (FXML): A tela é descrita em arquivo XML limpo (MainView.fxml), desacoplado da lógica de negócio. |
Permite edição visual com ferramentas WYSIWYG (Scene Builder) e trabalho paralelo entre designers e desenvolvedores. |
| Estilização Visual | Código Java rígido (btn.setBackground(new Color(...))) ou Look & Feel global. |
CSS Nativo: Estilização via propriedades como -fx-base: #4CAF50; e regras de classes visuais. |
Separação total entre estética visual e lógica de comportamento (idêntico à Web). |
| Padrão Arquitetural | Monolítico: a janela (JFrame) acumulava desenho de tela, tratamento de eventos e regras de apresentação. |
MVC Estrito: View (.fxml), Controller (@FXML Controller), Model (Tarefa) e Service (TarefaApiService). |
Alta manutenibilidade, testabilidade e baixo acoplamento. |
| Gerenciamento de Listas | DefaultTableModel e tableModel.addRow(), exigindo repaints manuais. |
Data Binding & Reatividade: ObservableList<Tarefa> e PropertyValueFactory. Ao alterar a lista observável, a TableView atualiza sozinha. |
Elimina código repetitivo de sincronização de tela (boilerplate). |
| Sistema de Módulos (JPMS) | Classpath clássico (Java 8 style), sem isolamento estrito de pacotes. | Modular (Java 9+): Descrito no module-info.java, controlando explicitamente dependências e permissões de reflexão. |
Máxima segurança de encapsulamento e preparação para criação de instaladores nativos com jlink/jpackage. |
flowchart TD
subgraph View ["🖼️ Camada de Visão (Declarativa)"]
FXML["📄 MainView.fxml (Layout XML)"]
CSS["🎨 CSS (Estilos Visuais)"]
FXML --- CSS
end
subgraph ControllerLayer ["🧠 Camada de Controle"]
Ctrl["⚙️ MainViewController.java<br>(Eventos @FXML e Data Binding)"]
ObsList["📊 ObservableList<Tarefa><br>(Lista Reativa)"]
Ctrl --- ObsList
end
subgraph ModelLayer ["📦 Camada de Modelo"]
Model["📋 Tarefa.java<br>(Propriedades JavaFX / DTO)"]
end
subgraph ServiceLayer ["🌐 Camada de Comunicação HTTP"]
Service["🔌 TarefaApiService.java<br>(HttpClient + Jackson)"]
end
subgraph BackendCentral ["⚙️ API REST Spring Boot (Porta 8080) — SSOT"]
API["🚀 http://localhost:8080/api/tarefas"]
end
FXML -->|Injeta componentes @FXML| Ctrl
Ctrl -->|Manipula| Model
Ctrl -->|Dispara operações assíncronas| Service
Service <-->|JSON REST| API
ObsList -->|Atualiza automaticamente| FXML
style View fill:#f8fafc,stroke:#64748b,stroke-width:2px
style ControllerLayer fill:#eff6ff,stroke:#3b82f6,stroke-width:2px
style ModelLayer fill:#fef3c7,stroke:#f59e0b
style ServiceLayer fill:#f0fdf4,stroke:#10b981
style BackendCentral fill:#faf5ff,stroke:#8b5cf6,stroke-width:2px
| ID | Nome do Requisito | Descrição Detalhada | Endpoint REST |
|---|---|---|---|
| RF-FX01 | Carga de Dados na Inicialização | Consultar tarefas da API via TarefaApiService e popular a ObservableList<Tarefa>. |
GET /api/tarefas |
| RF-FX02 | Inclusão de Nova Tarefa | Validar campo de texto, despachar POST /api/tarefas e adicionar item à tabela. |
POST /api/tarefas |
| RF-FX03 | Toggle de Conclusão | Alternar valor booleano e enviar PUT /api/tarefas/{id} sincronizando o status. |
PUT /api/tarefas/{id} |
| RF-FX04 | Remoção de Tarefa | Confirmar ação e enviar DELETE /api/tarefas/{id}, removendo da lista observável. |
DELETE /api/tarefas/{id} |
| RF-FX05 | Sincronização Manual | Fornecer botão de recarga para buscar atualizações feitas por clientes Web ou Mobile. | GET /api/tarefas |
| ID | Categoria | Critério Técnico |
|---|---|---|
| RNF-FX01 | Arquitetura MVC Estrita | Desacoplamento total entre visão FXML, lógica de controle e serviços HTTP. |
| RNF-FX02 | Thread Safety (JavaFX UI Thread) | Todo retorno de rede assíncrono deve ser despachado via Platform.runLater(). |
| RNF-FX03 | Conformidade Modular (JPMS) | O arquivo module-info.java deve declarar expressamente requires e opens. |
| RNF-FX04 | Estilização com CSS Nativo | 100% da identidade visual configurada via folha de estilo styles.css. |
| RNF-FX05 | Serialização JSON Moderna | Utilizar com.fasterxml.jackson.databind para conversão de DTOs. |
flowchart LR
User(["👤 Operador Desktop (JavaFX)"])
subgraph AppFX ["💻 Cliente Desktop JavaFX"]
direction TB
UC01["UC01: Consultar Tarefas na TableView"]
UC02["UC02: Adicionar Tarefa via Formulário"]
UC03["UC03: Alternar Status de Conclusão"]
UC04["UC04: Excluir Tarefa Selecionada"]
UC05["UC05: Sincronizar com Servidor"]
end
subgraph BackendAPI ["⚙️ Spring Boot API (Porta 8080)"]
E1["GET /api/tarefas"]
E2["POST /api/tarefas"]
E3["PUT /api/tarefas/{id}"]
E4["DELETE /api/tarefas/{id}"]
end
User --> UC01
User --> UC02
User --> UC03
User --> UC04
User --> UC05
UC01 -.-> E1
UC02 -.-> E2
UC03 -.-> E3
UC04 -.-> E4
UC05 -.-> E1
| História de Usuário | Requisito Funcional | Requisito Não-Funcional | Caso de Uso | Componente JavaFX |
|---|---|---|---|---|
| US-FX01 (Listagem Reativa) | RF-FX01 | RNF-FX01, RNF-FX02 | UC01 | MainViewController.java, MainView.fxml |
| US-FX02 (Cadastro Direto) | RF-FX02 | RNF-FX01, RNF-FX05 | UC02 | MainViewController.java, TarefaApiService.java |
| US-FX03 (Conclusão Rápida) | RF-FX03 | RNF-FX02, RNF-FX05 | UC03 | MainViewController.java, TarefaApiService.java |
| US-FX04 (Exclusão Segura) | RF-FX04 | RNF-FX01, RNF-FX05 | UC04 | MainViewController.java |
| US-FX05 (Concorrência Segura) | RF-FX05 | RNF-FX02, RNF-FX03 | UC05 | TarefaApiService.java, Platform.runLater() |
module-info.java)No Java 17+, aplicações corporativas utilizam o JPMS (Java Platform Module System). O JavaFX exige declarar os módulos requeridos e abrir os pacotes para reflexão de bibliotecas externas (como o motor FXML e o Jackson Databind):
src/main/java/module-info.javamodule br.com.curso.listadetarefas.desktop {
requires javafx.controls;
requires javafx.fxml;
requires java.net.http;
requires com.fasterxml.jackson.databind;
// Permite que o JavaFX e o Jackson acessem classes e campos privados via reflexao
opens br.com.curso.listadetarefas.desktop to javafx.fxml, com.fasterxml.jackson.databind;
exports br.com.curso.listadetarefas.desktop;
}
💡 Por que o
opensé obrigatório? O parser FXML precisa instanciar oMainViewControllere injetar campos anotados com@FXML. Da mesma forma, o Jackson precisa inspecionar construtores e métodos deTarefa.java. No sistema de módulos do Java, se você não declararopens ... to ..., a JVM bloqueará o acesso por segurança e lançará umIllegalAccessException.
pom.xml)O pom.xml gerencia as dependências do OpenJFX 21, do Jackson e o Maven Shade Plugin:
pom.xml<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>br.com.curso</groupId>
<artifactId>desktop-javafx</artifactId>
<version>1.0-SNAPSHOT</version>
<name>desktop-javafx</name>
<description>Cliente Desktop Moderno em JavaFX com FXML, CSS e MVC</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<javafx.version>17.0.6</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<!-- Plugin Shade para empacotamento com MainClass apontando para Launcher -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>br.com.curso.listadetarefas.desktop.Launcher</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Tarefa.java)Diferente do POJO estático do Swing, no JavaFX utilizamos propriedades observáveis (BooleanProperty) para permitir que alterações de checkbox sejam refletidas automaticamente:
src/main/java/br/com/curso/listadetarefas/desktop/Tarefa.javapackage br.com.curso.listadetarefas.desktop;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Tarefa {
private Long id;
private String descricao;
private boolean concluida;
// Propriedade observavel para controle de selecao na TableView
private final BooleanProperty selecionada = new SimpleBooleanProperty(false);
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDescricao() { return descricao; }
public void setDescricao(String descricao) { this.descricao = descricao; }
public boolean isConcluida() { return concluida; }
public void setConcluida(boolean concluida) { this.concluida = concluida; }
public boolean isSelecionada() { return selecionada.get(); }
public BooleanProperty selecionadaProperty() { return selecionada; }
public void setSelecionada(boolean selecionada) { this.selecionada.set(selecionada); }
}
MainView.fxml)Em vez de instanciar botões e layouts em código Java, a tela é declarada no arquivo XML:
src/main/resources/br/com/curso/listadetarefas/desktop/MainView.fxml<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.text.Font?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="br.com.curso.listadetarefas.desktop.MainViewController">
<top>
<HBox alignment="CENTER_LEFT" prefHeight="70.0" spacing="10.0" BorderPane.alignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<children>
<Label text="Nova Tarefa:">
<font>
<Font name="System Bold" size="14.0" />
</font>
</Label>
<TextField fx:id="novaTarefaTextField" HBox.hgrow="ALWAYS" onAction="#adicionarTarefa" promptText="O que precisa ser feito?" />
<Button onAction="#adicionarTarefa" style="-fx-base: #4CAF50;" text="Adicionar" textFill="WHITE">
<font>
<Font name="System Bold" size="12.0" />
</font>
</Button>
<Button onAction="#carregarTarefas" style="-fx-base: #2196F3;" text="🔄 Atualizar" textFill="WHITE">
<font>
<Font name="System Bold" size="12.0" />
</font>
</Button>
</children>
</HBox>
</top>
<center>
<TableView fx:id="tabelaTarefas" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<columns>
<TableColumn fx:id="colunaSelecao" prefWidth="50.0" text="Sel." />
<TableColumn fx:id="colunaConcluida" prefWidth="75.0" text="Status" />
<TableColumn fx:id="colunaDescricao" prefWidth="450.0" text="Descrição" />
<TableColumn fx:id="colunaAcoes" prefWidth="150.0" text="Ações" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
</center>
</BorderPane>
MainViewController.java)O controlador vincula a View XML aos dados e às ações do usuário através da anotação @FXML:
src/main/java/br/com/curso/listadetarefas/desktop/MainViewController.javapackage br.com.curso.listadetarefas.desktop;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import java.net.URL;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
public class MainViewController implements Initializable {
@FXML private TextField novaTarefaTextField;
@FXML private TableView<Tarefa> tabelaTarefas;
@FXML private TableColumn<Tarefa, Boolean> colunaSelecao;
@FXML private TableColumn<Tarefa, Boolean> colunaConcluida;
@FXML private TableColumn<Tarefa, String> colunaDescricao;
@FXML private TableColumn<Tarefa, Void> colunaAcoes;
private final TarefaApiService apiService = new TarefaApiService();
private final ObservableList<Tarefa> tarefasObservableList = FXCollections.observableArrayList();
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
configurarTabela();
carregarTarefas();
}
private void configurarTabela() {
colunaSelecao.setCellValueFactory(cellData -> cellData.getValue().selecionadaProperty());
colunaSelecao.setCellFactory(CheckBoxTableCell.forTableColumn(colunaSelecao));
colunaSelecao.setEditable(true);
colunaConcluida.setCellValueFactory(new PropertyValueFactory<>("concluida"));
colunaConcluida.setCellFactory(col -> new TableCell<>() {
@Override
protected void updateItem(Boolean concluida, boolean empty) {
super.updateItem(concluida, empty);
if (empty || concluida == null) {
setText(null);
setStyle("");
} else {
setText(concluida ? "Concluída" : "Pendente");
setStyle(concluida ? "-fx-text-fill: #27ae60; -fx-font-weight: bold;" : "-fx-text-fill: #e67e22;");
}
}
});
colunaDescricao.setCellValueFactory(new PropertyValueFactory<>("descricao"));
configurarColunaAcoes();
tabelaTarefas.setItems(tarefasObservableList);
tabelaTarefas.setEditable(true);
}
private void configurarColunaAcoes() {
colunaAcoes.setCellFactory(param -> new TableCell<>() {
private final Button btnAlternar = new Button("✔️");
private final Button btnEditar = new Button("✏️");
private final Button btnExcluir = new Button("🗑️");
private final HBox pane = new HBox(5, btnAlternar, btnEditar, btnExcluir);
{
btnAlternar.setOnAction(e -> {
Tarefa t = getTableView().getItems().get(getIndex());
t.setConcluida(!t.isConcluida());
atualizarTarefa(t);
});
btnEditar.setOnAction(e -> {
Tarefa t = getTableView().getItems().get(getIndex());
abrirDialogEdicao(t);
});
btnExcluir.setOnAction(e -> {
Tarefa t = getTableView().getItems().get(getIndex());
confirmarExclusao(t);
});
}
@Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
setGraphic(empty ? null : pane);
}
});
}
@FXML
public void carregarTarefas() {
new Thread(() -> {
try {
List<Tarefa> tarefas = apiService.listarTarefas();
Platform.runLater(() -> tarefasObservableList.setAll(tarefas));
} catch (Exception e) {
Platform.runLater(() -> exibirAlerta("Erro de Rede", "Não foi possível carregar as tarefas da API."));
}
}).start();
}
@FXML
private void adicionarTarefa() {
String descricao = novaTarefaTextField.getText().trim();
if (descricao.isEmpty()) return;
Tarefa nova = new Tarefa();
nova.setDescricao(descricao);
nova.setConcluida(false);
new Thread(() -> {
apiService.adicionarTarefa(nova);
Platform.runLater(() -> {
novaTarefaTextField.clear();
carregarTarefas();
});
}).start();
}
private void atualizarTarefa(Tarefa tarefa) {
new Thread(() -> {
apiService.atualizarTarefa(tarefa);
Platform.runLater(this::carregarTarefas);
}).start();
}
private void abrirDialogEdicao(Tarefa tarefa) {
TextInputDialog dialog = new TextInputDialog(tarefa.getDescricao());
dialog.setTitle("Editar Tarefa");
dialog.setHeaderText("Editar descrição da tarefa #" + tarefa.getId());
dialog.setContentText("Nova descrição:");
dialog.showAndWait().ifPresent(novaDescricao -> {
if (!novaDescricao.trim().isEmpty()) {
tarefa.setDescricao(novaDescricao.trim());
atualizarTarefa(tarefa);
}
});
}
private void confirmarExclusao(Tarefa tarefa) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Confirmar Exclusão");
alert.setHeaderText("Excluir tarefa #" + tarefa.getId() + " (" + tarefa.getDescricao() + ")?");
alert.setContentText("Esta ação removerá a tarefa de forma permanente.");
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
new Thread(() -> {
apiService.deletarTarefa(tarefa.getId());
Platform.runLater(this::carregarTarefas);
}).start();
}
}
private void exibirAlerta(String titulo, String mensagem) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(titulo);
alert.setHeaderText(null);
alert.setContentText(mensagem);
alert.showAndWait();
}
}
Launcher.java e MainApp.java)No Java 11+, uma aplicação JavaFX empacotada em .jar executável não pode ter como classe principal uma subclasse direta de javafx.application.Application caso o runtime do JavaFX não esteja no bootclasspath da JVM. Criamos a classe Launcher.java para contornar essa restrição da plataforma:
src/main/java/br/com/curso/listadetarefas/desktop/Launcher.javapackage br.com.curso.listadetarefas.desktop;
/**
* Ponto de entrada padrão que contorna a checagem de módulos do JavaFX.
* Delega a execução para a classe MainApp que herda de Application.
*/
public class Launcher {
public static void main(String[] args) {
MainApp.main(args);
}
}
src/main/java/br/com/curso/listadetarefas/desktop/MainApp.javapackage br.com.curso.listadetarefas.desktop;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MainApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/br/com/curso/listadetarefas/desktop/MainView.fxml"));
Parent root = loader.load();
primaryStage.setTitle("Lista de Tarefas — Cliente Desktop JavaFX Moderno");
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
cd catalogo_projetos/fullstack_tarefas_09_spring_javafx/desktop-javafx
.\mvnw.cmd clean package
java -jar target/desktop-javafx-1.0-SNAPSHOT.jar
Execute o script listadetarefas-painel.ps1 na pasta do projeto:
.\listadetarefas-painel.ps1
1 para subir a API REST central da Parte 01.3 para subir o Frontend Web Svelte (JS Puro) da Parte 02.5 para abrir o Cliente Desktop JavaFX da Parte 09.| Checkpoint | Ação Realizada | Comando / Critério | Resultado Esperado | Status |
|---|---|---|---|---|
| CP-01 | Inicializar a aplicação JavaFX | java -jar desktop-javafx-1.0-SNAPSHOT.jar |
A janela carrega com estilo CSS e dados populados na TableView. |
PASS ✅ |
| CP-02 | Digitar nova tarefa e clicar em Adicionar | Preencher campo e clicar | Linha adicionada à ObservableList sem recarregar a tela; status 201 retornado. |
PASS ✅ |
| CP-03 | Selecionar linha e clicar em Concluir/Reabrir | Toggle de status | Chamada PUT confirmada; coluna de status reflete alteração com texto verde. |
PASS ✅ |
| CP-04 | Selecionar linha e clicar em Excluir | Confirmar exclusão | Chamada DELETE despachada; item removido da TableView instantaneamente. |
PASS ✅ |
| CP-05 | Clicar no botão Atualizar | Sincronização manual | A aplicação busca alterações feitas via Web ou Mobile sem travar a interface. | PASS ✅ |
Ao comparar os dois clientes desktop construídos ao longo da trilha:
Agora que você dominou a modernização da engenharia desktop no ecossistema Java, feche com chave de ouro a nossa esteira de 10 partes descobrindo como a indústria transformou o desenvolvimento nativo móvel: migrando do modelo clássico imperativo de Views XML da Parte 04 para o paradigma puramente declarativo e reativo em Kotlin + Jetpack Compose, Coroutines e Unidirectional Data Flow (UDF)!