📚 Pré-requisitos: É fundamental ter concluído a Parte 01 — API REST com Spring Boot e a Parte 02 — Cliente Web com Svelte (JavaScript), ou ter a API Spring Boot em execução na porta 8080.

🚀 Projeto Lista de Tarefas (Parte 03): Cliente Desktop com Java Swing

Bem-vindo à Parte 03 da Trilha Prática de Aplicações Full Stack!

Na Parte 01, você construiu a API REST central com Spring Boot e banco H2. Na Parte 02, conectamos o primeiro cliente desacoplado: uma Single Page Application em Svelte (JavaScript puro) rodando no navegador na porta 5173.

Agora, daremos um passo fundamental na compreensão de arquiteturas corporativas modernas: construiremos um Cliente Desktop Clássico com Java Swing que consome a mesmíssima API REST.

Trilha Prática de Aplicações Full Stack:
├── Parte 01: API REST com Spring Boot + YARC + Thymeleaf (SSR)      [Concluído]
├── Parte 02: Cliente Web Desacoplado com Svelte (JS Puro)           [Concluído]
├── Parte 03: Cliente Desktop Clássico com Java Swing + Multi-Cliente [ESTE PROJETO]
├── Parte 04: Cliente Mobile Nativo com Android (Kotlin)
├── Parte 05 a 08: Docker, Arquitetura, Vue.js, React e Testes
└── Parte 09: Evolução Arquitetural Desktop: De Swing para JavaFX Moderno

💡 Por que Começar com Java Swing no Desktop?

Para quem está tendo o primeiro contato com aplicações desktop que consom APIs, o Java Swing (javax.swing.*) oferece a melhor curva de aprendizado:

  1. Zero Burocracia de Módulos: O Swing é nativo do Java SE (incluso no JDK). Não exige module-info.java, flags especiais de máquina virtual (--add-modules), nem plugins pesados no Maven.
  2. Foco Conceitual Puro: O aluno foca no que realmente importa:
    • Como um programa Java faz requisições HTTP REST (GET, POST, PUT, DELETE).
    • Como converter dados JSON em objetos Java usando o Jackson.
    • Como manipular a Event Dispatch Thread (EDT) para que chamadas de rede nunca congelem a interface gráfica.
  3. A Base para a Próxima Evolução: Ao dominar a interface imperativa clássica do Swing nesta Parte 03, você estará plenamente capacitado para compreender a evolução arquitetural para JavaFX moderno com FXML e CSS na Parte 09.

📐 1. Engenharia de Software: Requisitos do Cliente Desktop

📝 1.1 Contexto de Negócio e Histórias de Usuário (User Stories)

No ambiente corporativo, operadores de escritório frequentemente preferem aplicativos desktop nativos por permitirem digitação contínua, atalhos de teclado e estabilidade sem depender de abas de navegador:


📋 1.2 Especificação de Requisitos Funcionais (RF) do Desktop

ID Nome do Requisito Descrição Detalhada Regra de Negócio / UX
RF-D01 Carregar Dados na Tabela Ao abrir o aplicativo, disparar GET /api/tarefas e preencher o JTable. Exibir indicador na barra de status: 🟢 Conectado ou 🔴 Desconectado.
RF-D02 Cadastrar Nova Tarefa Coletar o texto do JTextField, montar o objeto JSON e enviar via POST /api/tarefas. Impedir envio de campos vazios. Limpar o campo de texto após o sucesso.
RF-D03 Alternar Conclusão Inverter o status da tarefa selecionada e enviar via PUT /api/tarefas/{id}. Exigir que uma linha da tabela esteja previamente selecionada.
RF-D04 Editar Tarefa Abrir caixa de diálogo (JOptionPane) com a descrição atual e atualizar via PUT. Não permitir salvar descrição vazia.
RF-D05 Excluir Tarefa Disparar DELETE /api/tarefas/{id} e remover a linha da tabela após confirmação. Exibir confirmação (JOptionPane.showConfirmDialog).
RF-D06 Sincronização Manual Fornecer botão para recarregar a lista da API a qualquer momento. Garante que dados inseridos pelo Svelte, Thymeleaf ou YARC apareçam imediatamente na tela.

🛡️ 1.3 Especificação de Requisitos Não-Funcionais (RNF)

ID Categoria Descrição do Critério Justificativa Técnica
RNF-D01 Interface Nativa (Swing) A tela deve ser construída com javax.swing.* e Look and Feel nativo do sistema operacional. Padrão integrado ao Java SE, sem necessidade de módulos externos ou plugins adicionais.
RNF-D02 HTTP Client Nativo do Java A comunicação com a API deve utilizar java.net.http.HttpClient nativo do Java 11+. Elimina a necessidade de bibliotecas externas pesadas para transporte HTTP.
RNF-D03 Serialização JSON com Jackson A conversão entre JSON e objetos Java deve ser realizada pelo ObjectMapper do Jackson. Padrão corporativo para alto desempenho e total compatibilidade com o Spring Boot.
RNF-D04 Thread Safety na UI (EDT) Requisições HTTP em segundo plano (CompletableFuture) não podem travar a Event Dispatch Thread. Evita que a janela congele enquanto aguarda resposta da rede.
RNF-D05 Executável Portável (Fat JAR) A aplicação deve ser empacotada em um único arquivo .jar contendo todas as dependências. Facilita a distribuição e execução com um duplo clique ou comando java -jar.

🎭 1.4 Diagrama de Casos de Uso do Ecossistema Multi-Cliente

flowchart LR
    UserWeb(["👤 Usuário Web (Svelte)"])
    UserDesktop(["👤 Operador Desktop (Swing)"])

    subgraph CasosDeUso ["🎯 Casos de Uso Canônicos do Sistema"]
        direction TB
        UC01(["UC01: Consultar Tarefas"])
        UC02(["UC02: Cadastrar Nova Tarefa"])
        UC03(["UC03: Alternar Status Concluído"])
        UC04(["UC04: Editar Descrição"])
        UC05(["UC05: Excluir Tarefa"])
    end

    subgraph BackendCentral ["⚙️ API REST Spring Boot (Porta 8080) — SSOT"]
        direction TB
        Endpoint["🔌 Endpoints /api/tarefas (JSON)"]
        Service["🧠 TarefaService (Regras de Negócio)"]
        DB[("💾 Banco H2 em Memória")]

        Endpoint --> Service
        Service --> DB
    end

    UserWeb --> UC01 & UC02 & UC03 & UC04 & UC05
    UserDesktop --> UC01 & UC02 & UC03 & UC04 & UC05

    UC01 & UC02 & UC03 & UC04 & UC05 <-->|HTTP REST / JSON| Endpoint

    style CasosDeUso fill:#f8fafc,stroke:#334155,stroke-width:2px
    style BackendCentral fill:#f0fdf4,stroke:#16a34a,stroke-width:2px
Caso de Uso Descrição Requisitos Atendidos Disparo na UI Desktop
UC01 Carregar e exibir tabela de tarefas em tempo real RF-D01, RF-D06 Inicialização da tela ou botão 🔄 Atualizar
UC02 Inserir nova tarefa no banco de dados central RF-D02 Campo de texto + botão ➕ Adicionar ou Enter
UC03 Alternar status entre Pendente e Concluída RF-D03 Linha selecionada + botão ✔️ Concluir / Reabrir
UC04 Modificar descrição da tarefa existente RF-D04 Duplo clique na linha ou botão ✏️ Editar
UC05 Deletar tarefa permanentemente RF-D05 Linha selecionada + botão 🗑️ Excluir

🗺️ 1.5 Matriz de Rastreabilidade Multi-Cliente (RTM)

A tabela abaixo demonstra como a mesma API REST atende os mesmos requisitos funcionais tanto na Web (Svelte) quanto no Desktop (Swing):

Requisito No Cliente Web Svelte (Parte 02) No Cliente Desktop Swing (Parte 03) Endpoint da API REST Status HTTP
Listar Tarefas api.js: listarTarefas() tarefaApiService.listarTarefas() GET /api/tarefas 200 OK
Criar Tarefa api.js: criarTarefa() tarefaApiService.criarTarefa() POST /api/tarefas 201 Created
Alternar Status api.js: atualizarTarefa() tarefaApiService.alternarStatus() PUT /api/tarefas/{id} 200 OK
Editar Descrição Edição Inline no DOM (dblclick) tarefaApiService.atualizarDescricao() PUT /api/tarefas/{id} 200 OK
Excluir Tarefa api.js: excluirTarefa() tarefaApiService.excluirTarefa() DELETE /api/tarefas/{id} 204 No Content
Estrutura de Tela Componentes Svelte 5 (.svelte) JFrame + JTable + JPanel (Swing) — —
Biblioteca HTTP fetch nativo do Browser ES6+ Java java.net.http.HttpClient — —

🗺️ 2. Arquitetura da Aplicação e Visão Sistêmica

Observe com atenção o diagrama abaixo. Ele sintetiza o grande marco da arquitetura de software: o desacoplamento total entre frontends e backends.

Arquitetura Multi-Cliente Corporativa

  1. A API REST é a Fonte Única da Verdade: Nenhuma regra de negócio ou dado fica duplicado nos clientes.
  2. Independência Tecnológica: O cliente web roda em JavaScript/TypeScript no navegador; o cliente desktop roda em código nativo Java compilado na JVM.
  3. Sincronização Imediata: Se você cadastrar uma tarefa pelo Java Swing, ela é persistida na tabela TB_TAREFAS do H2 e fica imediatamente disponível para a aplicação Angular e para a interface Thymeleaf.

⚙️ 3. Construção do Cliente Desktop Swing Passo a Passo (Tutorial Autoguiado)

📂 Passo 1: Validação da API Central (Porta 8080) — Fonte Única da Verdade

Na arquitetura corporativa deste curso, a API REST reside na pasta canônica fullstack_tarefas_01_spring_api/api-spring.

Antes de abrir o projeto Desktop, certifique-se de que a API central está ativa:

  1. Abra o terminal na pasta da API central:
    cd ../fullstack_tarefas_01_spring_api/api-spring
    
  2. Inicie o servidor Spring Boot:
    • No Windows (PowerShell):
      .\mvnw.cmd spring-boot:run
      
    • No Linux/macOS:
      ./mvnw spring-boot:run
      
  3. Confirme que a API está pronta em http://localhost:8080/api/tarefas.

🎯 Checkpoint de Verificação 1: Backend Ativo


📦 Passo 2: Estrutura do Projeto e Dependências Maven (pom.xml)

O projeto desktop-swing é gerenciado pelo Maven de forma limpa, exigindo apenas o Jackson para conversão JSON:

🌳 Estrutura de Pastas do Projeto Desktop:

desktop-swing/
├── pom.xml                                           ← Configurações do Maven e Jackson
├── mvnw                                              ← Script Maven para Linux/macOS
├── mvnw.cmd                                          ← Script Maven para Windows
└── src/
    └── main/
        └── java/
            └── br/com/curso/listadetarefas/desktop/
                ├── Tarefa.java                       ← Modelo POJO compatível com Jackson
                ├── TarefaApiService.java             ← Cliente HTTP nativo (java.net.http)
                ├── TarefaSwingFrame.java             ← Janela principal JFrame com JTable e ações
                └── MainSwingApp.java                 ← Ponto de entrada com Look and Feel nativo

Arquivo: 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-swing</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>desktop-swing</name>
    <description>Cliente Desktop Clássico em Java Swing para a Lista de Tarefas</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <jackson.version>2.17.2</jackson.version>
    </properties>

    <dependencies>
        <!-- Jackson Databind para serialização e desserialização de JSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Plugin de Compilação Java SE Padrão -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>

            <!-- Plugin Shade para empacotar um JAR executável completo com dependências (Fat JAR) -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.3</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.MainSwingApp</mainClass>
                                </transformer>
                            </transformers>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

🎯 Checkpoint de Verificação 2: Dependências e Compilação do Maven


📝 Passo 3: O Modelo de Dados do Desktop (Tarefa.java)

No cliente desktop, criamos um modelo POJO limpo compatível com a desserialização do Jackson:

Arquivo: src/main/java/br/com/curso/listadetarefas/desktop/Tarefa.java

package br.com.curso.listadetarefas.desktop;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
 * Modelo de dados que representa uma Tarefa no cliente Desktop Swing.
 * Compatível com o payload JSON da API REST Spring Boot (/api/tarefas).
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class Tarefa {

    private Long id;
    private String descricao;
    private boolean concluida;

    public Tarefa() {
    }

    public Tarefa(Long id, String descricao, boolean concluida) {
        this.id = id;
        this.descricao = descricao;
        this.concluida = concluida;
    }

    public Tarefa(String descricao) {
        this(null, descricao, 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;
    }

    @Override
    public String toString() {
        return String.format("[%s] #%d - %s", (concluida ? "X" : " "), id != null ? id : 0, descricao);
    }
}

🌐 Passo 4: O Serviço HTTP Nativo (TarefaApiService.java)

Esta classe é o coração da comunicação de rede no Desktop. Ela utiliza a biblioteca nativa java.net.http.HttpClient do Java 11+ para executar as quatro operações do CRUD:

Arquivo: src/main/java/br/com/curso/listadetarefas/desktop/TarefaApiService.java

package br.com.curso.listadetarefas.desktop;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;

/**
 * Camada de serviço de rede responsável por consumir a API REST Spring Boot.
 * Utiliza o java.net.http.HttpClient nativo do Java 11+ e Jackson para JSON.
 */
public class TarefaApiService {

    private final String apiUrl;
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;

    public TarefaApiService() {
        this("http://localhost:8080/api/tarefas");
    }

    public TarefaApiService(String apiUrl) {
        this.apiUrl = apiUrl;
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(5))
                .build();
        this.objectMapper = new ObjectMapper();
    }

    // RF-D01: Listar todas as tarefas cadastradas (GET /api/tarefas)
    public List<Tarefa> listarTarefas() throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiUrl))
                .header("Accept", "application/json")
                .timeout(Duration.ofSeconds(5))
                .GET()
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 200) {
            return objectMapper.readValue(response.body(), new TypeReference<List<Tarefa>>() {});
        } else {
            throw new IOException("Falha ao listar tarefas. Código HTTP: " + response.statusCode());
        }
    }

    // RF-D02: Cadastrar uma nova tarefa (POST /api/tarefas)
    public Tarefa criarTarefa(String descricao) throws IOException, InterruptedException {
        Tarefa nova = new Tarefa(descricao);
        String jsonBody = objectMapper.writeValueAsString(nova);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiUrl))
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .timeout(Duration.ofSeconds(5))
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 201 || response.statusCode() == 200) {
            return objectMapper.readValue(response.body(), Tarefa.class);
        } else {
            throw new IOException("Falha ao criar tarefa. Código HTTP: " + response.statusCode());
        }
    }

    // RF-D03: Alternar status de conclusão (PUT /api/tarefas/{id})
    public Tarefa alternarStatus(Tarefa tarefa) throws IOException, InterruptedException {
        tarefa.setConcluida(!tarefa.isConcluida());
        String jsonBody = objectMapper.writeValueAsString(tarefa);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiUrl + "/" + tarefa.getId()))
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .timeout(Duration.ofSeconds(5))
                .PUT(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 200) {
            return objectMapper.readValue(response.body(), Tarefa.class);
        } else {
            throw new IOException("Falha ao alternar status da tarefa #" + tarefa.getId() + ". Código: " + response.statusCode());
        }
    }

    // RF-D04: Atualizar a descrição da tarefa (PUT /api/tarefas/{id})
    public Tarefa atualizarDescricao(Long id, String novaDescricao, boolean concluida) throws IOException, InterruptedException {
        Tarefa atualizada = new Tarefa(id, novaDescricao, concluida);
        String jsonBody = objectMapper.writeValueAsString(atualizada);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiUrl + "/" + id))
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .timeout(Duration.ofSeconds(5))
                .PUT(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 200) {
            return objectMapper.readValue(response.body(), Tarefa.class);
        } else {
            throw new IOException("Falha ao atualizar tarefa #" + id + ". Código: " + response.statusCode());
        }
    }

    // RF-D05: Excluir uma tarefa por ID (DELETE /api/tarefas/{id})
    public void excluirTarefa(Long id) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiUrl + "/" + id))
                .timeout(Duration.ofSeconds(5))
                .DELETE()
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 204 && response.statusCode() != 200) {
            throw new IOException("Falha ao excluir tarefa #" + id + ". Código HTTP: " + response.statusCode());
        }
    }
}

🎯 Checkpoint de Verificação 3: Comunicação REST sem Bibliotecas Externas


🎨 Passo 5: A Interface Gráfica Swing com JTable (TarefaSwingFrame.java)

Construímos a janela principal (JFrame) utilizando componentes clássicos do Swing com renderização customizada para badges de status e execução assíncrona (CompletableFuture + SwingUtilities.invokeLater):

Arquivo: src/main/java/br/com/curso/listadetarefas/desktop/TarefaSwingFrame.java

package br.com.curso.listadetarefas.desktop;

import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * Interface Gráfica Principal do Cliente Desktop desenvolvida em Java Swing puro.
 * Conecta-se à API REST Spring Boot na porta 8080 e sincroniza operações CRUD.
 */
public class TarefaSwingFrame extends JFrame {

    private final TarefaApiService apiService;
    private final List<Tarefa> tarefasList = new ArrayList<>();

    // Componentes de UI
    private JTextField txtNovaTarefa;
    private JButton btnAdicionar;
    private JTable tabelaTarefas;
    private DefaultTableModel tableModel;
    private JLabel lblStatusConexao;
    private JButton btnAlternar;
    private JButton btnEditar;
    private JButton btnExcluir;
    private JButton btnAtualizar;

    public TarefaSwingFrame() {
        this.apiService = new TarefaApiService();
        initComponents();
        carregarTarefasAssincrono();
    }

    private void initComponents() {
        setTitle("Lista de Tarefas — Cliente Desktop Swing");
        setSize(750, 520);
        setMinimumSize(new Dimension(650, 420));
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout(10, 10));

        // 1. Cabeçalho (Topo)
        JPanel painelTopo = new JPanel(new BorderLayout(5, 5));
        painelTopo.setBorder(BorderFactory.createEmptyBorder(12, 16, 5, 16));

        JLabel lblTitulo = new JLabel("📝 Minhas Tarefas (Desktop Swing)");
        lblTitulo.setFont(new Font("Segoe UI", Font.BOLD, 18));
        lblTitulo.setForeground(new Color(25, 42, 86));

        lblStatusConexao = new JLabel("🔄 Conectando à API (http://localhost:8080)...");
        lblStatusConexao.setFont(new Font("Segoe UI", Font.PLAIN, 12));
        lblStatusConexao.setForeground(Color.GRAY);

        painelTopo.add(lblTitulo, BorderLayout.NORTH);
        painelTopo.add(lblStatusConexao, BorderLayout.SOUTH);

        // 2. Formulário de Cadastro (Acima da Tabela)
        JPanel painelForm = new JPanel(new BorderLayout(8, 8));
        painelForm.setBorder(BorderFactory.createEmptyBorder(5, 16, 10, 16));

        txtNovaTarefa = new JTextField();
        txtNovaTarefa.setFont(new Font("Segoe UI", Font.PLAIN, 14));
        txtNovaTarefa.setToolTipText("Digite o que você precisa fazer e pressione Enter...");

        btnAdicionar = new JButton("➕ Adicionar");
        btnAdicionar.setFont(new Font("Segoe UI", Font.BOLD, 13));
        btnAdicionar.setBackground(new Color(46, 204, 113));
        btnAdicionar.setForeground(Color.WHITE);
        btnAdicionar.setFocusPainted(false);
        btnAdicionar.setCursor(new Cursor(Cursor.HAND_CURSOR));

        painelForm.add(new JLabel("Nova Tarefa:"), BorderLayout.WEST);
        painelForm.add(txtNovaTarefa, BorderLayout.CENTER);
        painelForm.add(btnAdicionar, BorderLayout.EAST);

        JPanel containerNorte = new JPanel(new BorderLayout());
        containerNorte.add(painelTopo, BorderLayout.NORTH);
        containerNorte.add(painelForm, BorderLayout.SOUTH);
        add(containerNorte, BorderLayout.NORTH);

        // 3. Tabela Central com Lista de Tarefas
        String[] colunas = {"ID", "Descrição da Tarefa", "Status"};
        tableModel = new DefaultTableModel(colunas, 0) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        };

        tabelaTarefas = new JTable(tableModel);
        tabelaTarefas.setFont(new Font("Segoe UI", Font.PLAIN, 14));
        tabelaTarefas.setRowHeight(28);
        tabelaTarefas.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        tabelaTarefas.getTableHeader().setFont(new Font("Segoe UI", Font.BOLD, 13));

        tabelaTarefas.getColumnModel().getColumn(0).setPreferredWidth(60);
        tabelaTarefas.getColumnModel().getColumn(0).setMaxWidth(80);
        tabelaTarefas.getColumnModel().getColumn(1).setPreferredWidth(450);
        tabelaTarefas.getColumnModel().getColumn(2).setPreferredWidth(120);
        tabelaTarefas.getColumnModel().getColumn(2).setMaxWidth(140);

        // Renderizador customizado para status
        tabelaTarefas.getColumnModel().getColumn(2).setCellRenderer(new DefaultTableCellRenderer() {
            @Override
            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
                Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                setHorizontalAlignment(JLabel.CENTER);
                String status = value != null ? value.toString() : "";
                if ("Concluída".equals(status)) {
                    setForeground(new Color(39, 174, 96));
                    setFont(getFont().deriveFont(Font.BOLD));
                } else {
                    setForeground(new Color(230, 126, 34));
                    setFont(getFont().deriveFont(Font.PLAIN));
                }
                if (isSelected) {
                    setBackground(table.getSelectionBackground());
                } else {
                    setBackground(row % 2 == 0 ? Color.WHITE : new Color(248, 249, 250));
                }
                return c;
            }
        });

        JScrollPane scrollPane = new JScrollPane(tabelaTarefas);
        scrollPane.setBorder(BorderFactory.createEmptyBorder(0, 16, 0, 16));
        add(scrollPane, BorderLayout.CENTER);

        // 4. Rodapé com Botões de Ação
        JPanel painelRodape = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 12));
        painelRodape.setBorder(BorderFactory.createEmptyBorder(5, 16, 10, 16));

        btnAlternar = new JButton("✔️ Concluir / Reabrir");
        btnAlternar.setFont(new Font("Segoe UI", Font.PLAIN, 13));
        btnAlternar.setCursor(new Cursor(Cursor.HAND_CURSOR));

        btnEditar = new JButton("✏️ Editar");
        btnEditar.setFont(new Font("Segoe UI", Font.PLAIN, 13));
        btnEditar.setCursor(new Cursor(Cursor.HAND_CURSOR));

        btnExcluir = new JButton("🗑️ Excluir");
        btnExcluir.setFont(new Font("Segoe UI", Font.PLAIN, 13));
        btnExcluir.setForeground(new Color(192, 57, 43));
        btnExcluir.setCursor(new Cursor(Cursor.HAND_CURSOR));

        btnAtualizar = new JButton("🔄 Atualizar");
        btnAtualizar.setFont(new Font("Segoe UI", Font.PLAIN, 13));
        btnAtualizar.setCursor(new Cursor(Cursor.HAND_CURSOR));

        painelRodape.add(btnAlternar);
        painelRodape.add(btnEditar);
        painelRodape.add(btnExcluir);
        painelRodape.add(btnAtualizar);
        add(painelRodape, BorderLayout.SOUTH);

        // 5. Registro de Eventos
        btnAdicionar.addActionListener(e -> acaoAdicionar());
        txtNovaTarefa.addActionListener(e -> acaoAdicionar());
        btnAlternar.addActionListener(e -> acaoAlternarStatus());
        btnEditar.addActionListener(e -> acaoEditar());
        btnExcluir.addActionListener(e -> acaoExcluir());
        btnAtualizar.addActionListener(e -> carregarTarefasAssincrono());

        tabelaTarefas.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() == 2) {
                    acaoEditar();
                }
            }
        });
    }

    public void carregarTarefasAssincrono() {
        lblStatusConexao.setText("🔄 Sincronizando com a API...");
        lblStatusConexao.setForeground(Color.BLUE);

        CompletableFuture.supplyAsync(() -> {
            try {
                return apiService.listarTarefas();
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }).thenAccept(tarefas -> SwingUtilities.invokeLater(() -> {
            tarefasList.clear();
            tarefasList.addAll(tarefas);
            tableModel.setRowCount(0);
            for (Tarefa t : tarefas) {
                tableModel.addRow(new Object[]{t.getId(), t.getDescricao(), t.isConcluida() ? "Concluída" : "Pendente"});
            }
            lblStatusConexao.setText("🟢 Conectado à API (http://localhost:8080) — " + tarefas.size() + " tarefas sincronizadas");
            lblStatusConexao.setForeground(new Color(39, 174, 96));
        })).exceptionally(ex -> {
            SwingUtilities.invokeLater(() -> {
                lblStatusConexao.setText("🔴 Falha ao conectar na API Spring Boot (Porta 8080)");
                lblStatusConexao.setForeground(new Color(192, 57, 43));
            });
            return null;
        });
    }

    private void acaoAdicionar() {
        String descricao = txtNovaTarefa.getText().trim();
        if (descricao.isEmpty()) {
            JOptionPane.showMessageDialog(this, "Por favor, digite a descrição da tarefa!", "Aviso", JOptionPane.WARNING_MESSAGE);
            return;
        }

        btnAdicionar.setEnabled(false);
        CompletableFuture.runAsync(() -> {
            try {
                apiService.criarTarefa(descricao);
                SwingUtilities.invokeLater(() -> {
                    txtNovaTarefa.setText("");
                    carregarTarefasAssincrono();
                });
            } catch (Exception ex) {
                SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao criar tarefa: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
            } finally {
                SwingUtilities.invokeLater(() -> btnAdicionar.setEnabled(true));
            }
        });
    }

    private void acaoAlternarStatus() {
        int selectedRow = tabelaTarefas.getSelectedRow();
        if (selectedRow < 0) {
            JOptionPane.showMessageDialog(this, "Selecione uma tarefa na tabela para alterar o status!", "Aviso", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        Tarefa tarefa = tarefasList.get(selectedRow);
        CompletableFuture.runAsync(() -> {
            try {
                apiService.alternarStatus(tarefa);
                SwingUtilities.invokeLater(this::carregarTarefasAssincrono);
            } catch (Exception ex) {
                SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao alterar status: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
            }
        });
    }

    private void acaoEditar() {
        int selectedRow = tabelaTarefas.getSelectedRow();
        if (selectedRow < 0) {
            JOptionPane.showMessageDialog(this, "Selecione uma tarefa na tabela para editar!", "Aviso", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        Tarefa tarefa = tarefasList.get(selectedRow);
        String novaDescricao = JOptionPane.showInputDialog(this, "Editar descrição da tarefa #" + tarefa.getId() + ":", tarefa.getDescricao());

        if (novaDescricao != null && !novaDescricao.trim().isEmpty()) {
            CompletableFuture.runAsync(() -> {
                try {
                    apiService.atualizarDescricao(tarefa.getId(), novaDescricao.trim(), tarefa.isConcluida());
                    SwingUtilities.invokeLater(this::carregarTarefasAssincrono);
                } catch (Exception ex) {
                    SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao editar tarefa: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
                }
            });
        }
    }

    private void acaoExcluir() {
        int selectedRow = tabelaTarefas.getSelectedRow();
        if (selectedRow < 0) {
            JOptionPane.showMessageDialog(this, "Selecione uma tarefa na tabela para excluir!", "Aviso", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        Tarefa tarefa = tarefasList.get(selectedRow);
        int confirm = JOptionPane.showConfirmDialog(this, "Tem certeza que deseja excluir a tarefa #" + tarefa.getId() + " (" + tarefa.getDescricao() + ")?", "Confirmação", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);

        if (confirm == JOptionPane.YES_OPTION) {
            CompletableFuture.runAsync(() -> {
                try {
                    apiService.excluirTarefa(tarefa.getId());
                    SwingUtilities.invokeLater(this::carregarTarefasAssincrono);
                } catch (Exception ex) {
                    SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "Erro ao excluir tarefa: " + ex.getMessage(), "Erro", JOptionPane.ERROR_MESSAGE));
                }
            });
        }
    }
}

🚀 Passo 6: O Ponto de Entrada (MainSwingApp.java)

Configuramos o Look and Feel nativo do sistema operacional e despachamos a inicialização para a Event Dispatch Thread:

Arquivo: src/main/java/br/com/curso/listadetarefas/desktop/MainSwingApp.java

package br.com.curso.listadetarefas.desktop;

import javax.swing.SwingUtilities;
import javax.swing.UIManager;

/**
 * Ponto de entrada (Main) do Cliente Desktop Java Swing.
 * Configura o Look and Feel nativo do sistema operacional e inicializa a janela na Event Dispatch Thread (EDT).
 */
public class MainSwingApp {

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ignored) {
        }

        SwingUtilities.invokeLater(() -> {
            TarefaSwingFrame frame = new TarefaSwingFrame();
            frame.setVisible(true);
        });
    }
}

🎯 Checkpoint de Verificação 4: Construção do Pacote Executável


🚀 4. Como Executar no Laboratório e Testar a Sincronização

Opção 1: Execução Visual Direta pelo Terminal

Abra o terminal na pasta desktop-swing e execute o .jar gerado:

java -jar target/desktop-swing-1.0-SNAPSHOT.jar

A janela nativa do Swing abrirá imediatamente conectando-se à API!


Opção 2: Pelo Painel Integrado PowerShell

Execute o script listadetarefas-painel.ps1:

.\listadetarefas-painel.ps1

No menu interativo:


🧪 5. Roteiro de Testes Multi-Cliente em Tempo Real

  1. Teste de Persistência Cruzada (Desktop $\rightarrow$ Web):
    • Na janela do Swing, adicione a tarefa: “Apresentar demo multi-cliente”.
    • Vá ao navegador em http://localhost:5173 (Svelte Web da Parte 02) ou http://localhost:8080/tarefas (Thymeleaf da Parte 01) e recarregue a página. A tarefa estará lá!
  2. Teste de Edição Cruzada (Web $\rightarrow$ Desktop):
    • No cliente Web Svelte, conclua a tarefa marcando o checkbox.
    • Na janela do Swing, clique no botão 🔄 Atualizar. A coluna Status mudará instantaneamente para Concluída em verde!
  3. Teste de Duplo Clique:
    • No Swing, dê um duplo clique sobre qualquer tarefa para abrir a caixa de edição rápida e alterar o texto.

Matriz de Homologação & Checkpoints de Validação

Checkpoint Ação / Cenário de Teste Critério de Aceite Status
CP-01 Inicializar o app via java -jar com a API ativa Janela Swing carrega dados da API; barra de status exibe 🟢 Conectado à API. PASS ✅
CP-02 Digitar “Validar cliente Swing” e clicar em Adicionar Requisição POST disparada; nova linha surge na tabela com ID atribuído pela API. PASS ✅
CP-03 Selecionar linha e clicar em “Concluir / Reabrir” Status muda para “Concluída” em verde; API recebe PUT /api/tarefas/{id}. PASS ✅
CP-04 Dar duplo clique na descrição e alterar texto no diálogo Texto da tarefa é atualizado in-place no servidor e na linha da JTable. PASS ✅
CP-05 Selecionar linha, clicar em “Excluir” e confirmar no diálogo Requisição DELETE enviada; registro é removido do banco H2 e da tabela. PASS ✅

🧯 6. Se der errado (Resolução de Problemas Comuns)

Sintoma Causa Mais Provável Solução
🔴 Falha ao conectar na API Spring Boot A API na porta 8080 está desligada. Inicie a API na pasta canônica fullstack_tarefas_01_spring_api/api-spring.
UnsupportedClassVersionError Versão do Java inferior à 17. Verifique sua instalação com java --version e garanta JDK 17 ou superior.
A tabela não atualiza após mudanças na Web O cliente Swing não possui websockets nesta etapa. Clique no botão 🔄 Atualizar para buscar as alterações recentes da API.

🌉 7. Conclusão & Próximos Passos da Trilha

Parabéns! Você construiu com sucesso um cliente desktop nativo clássico que opera como um cidadão de primeira classe no ecossistema corporativo.

Agora, a sua jornada de engenharia de software ramifica-se em duas direções complementares:

👉 Sequência Cronológica: Parte 04 — Mobile Android Clássico (Java + Views XML)

Leve a mesmíssima API Spring Boot para o ambiente dos smartphones! Na Parte 04, você aprenderá a lidar com a virtualização do emulador Android (10.0.2.2:8080), layouts em XML e o componente de alta performance RecyclerView.

🖥️ Evolução Arquitetural Desktop: Parte 09 — De Swing para JavaFX Moderno

Quando desejar dominar a modernização da interface desktop corporativa, descubra na Parte 09 como substituir código Java imperativo por layouts declarativos em FXML, estilização com CSS, propriedades reativas (ObservableList) e arquitetura MVC rigorosa.