Extensão — JavaFX: Cenas, Controles e FXML

Extensão — JavaFX: Cenas, Controles e FXML

Nesta aula a janela de catálogo é refeita em JavaFX. A estrutura fica num arquivo FXML, a aparência em CSS e o comportamento num controlador Java com propriedades e bindings. As tarefas em segundo plano usam Task, e as dependências entram por uma fábrica de controladores.
Java

• • 22 min de leitura

Nesta aula a janela de catálogo é refeita em JavaFX. A estrutura fica num arquivo FXML, a aparência em CSS e o comportamento num controlador Java com propriedades e bindings. As tarefas em segundo plano usam Task, e as dependências entram por uma fábrica de controladores.

Na aula anterior o Swing resolveu o problema, mas com muita cerimônia: um modelo de tabela escrito à mão, três métodos de DocumentListener para um só evento e cada pedaço da tela atualizado à mão no lugar certo. O JavaFX foi desenhado mais de uma década depois, com outras respostas para as mesmas perguntas. A principal delas é que a interface não é atualizada pelo código: ela é declarada como uma função dos dados. Quando os dados mudam, a interface acompanha.

Preparando o projeto

Desde o JDK 11 o JavaFX é uma dependência externa. Com Maven:

<dependencies>
    <dependency>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-controls</artifactId>
        <version>27</version>
    </dependency>
    <dependency>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-fxml</artifactId>
        <version>27</version>
    </dependency>
    <dependency>                         <!-- só para o snapshot do teste -->
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-swing</artifactId>
        <version>27</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-maven-plugin</artifactId>
            <version>0.0.8</version>
            <configuration>
                <mainClass>br.com.exemplo.catalogo/br.com.exemplo.catalogo.CatalogoApp</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>

O artefato do Maven traz as bibliotecas nativas da plataforma em que você compila. mvn javafx:run roda o programa, e mvn javafx:jlink monta um runtime enxuto com a aplicação, pronto para o jpackage.

O JavaFX é modular, e a aplicação também deve ser. Este é o module-info.java (o sistema de módulos de "Fronteiras Explícitas — o Sistema de Módulos"):

module br.com.exemplo.catalogo {
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.swing;   // só para o snapshot do teste (SwingFXUtils e ImageIO)

    // o launcher do JavaFX instancia a classe Application por reflexão
    exports br.com.exemplo.catalogo to javafx.graphics;
    // o FXMLLoader injeta campos privados @FXML e chama métodos privados por reflexão
    opens br.com.exemplo.catalogo to javafx.fxml;
}

As duas últimas linhas resolvem os dois erros que todo iniciante em JavaFX encontra. Sem o exports, o launcher não consegue criar a sua Application. Sem o opens, o FXMLLoader falha com um LoadException ao tentar preencher um campo @FXML private, e a causa, lá embaixo no stack trace, é uma InaccessibleObjectException que diz exatamente o que falta: module br.com.exemplo.catalogo does not "opens br.com.exemplo.catalogo" to module javafx.fxml. Um terceiro erro clássico é "JavaFX runtime components are missing", que aparece quando o JavaFX está no classpath e não no module path. O plugin do Maven cuida disso para você.

No JDK 25 aparecem ainda avisos sobre acesso nativo, porque o JavaFX carrega bibliotecas nativas (a regra de "Ponte para o Nativo — a Foreign Function & Memory API"). A opção --enable-native-access=javafx.graphics os silencia de forma explícita.

O vocabulário: palco, cena e grafo

O JavaFX usa a metáfora do teatro:

  • O Stage (palco) é a janela.
  • A Scene (cena) é o conteúdo do palco, e pode ser trocada. Um assistente de várias etapas troca a cena a cada passo.
  • O grafo de cena (scene graph) é a árvore de nós (Node) que forma a cena. Nós que contêm outros nós são Parent: os painéis de layout (BorderPane, HBox, VBox, GridPane, StackPane) e os controles compostos. As folhas são controles (Button, TextField, Label, TableView) e formas (Rectangle, Circle, Text).

Ao contrário do Swing, você não desenha: você monta a árvore, e o JavaFX a desenha com aceleração de GPU. Mover um nó, girá-lo, mudar sua opacidade ou animá-lo é mudar uma propriedade dele.

O ciclo de vida de uma aplicação também tem nome e ordem. O launch() inicia o JavaFX e chama init() numa thread de apoio. Depois chama start(Stage) na JavaFX Application Thread, onde a interface nasce, e por fim stop(), quando a última janela fecha. É o equivalente ao invokeLater do Swing, mas embutido: o start já roda na thread certa.

Propriedades e bindings: a ideia central

No JavaFX, quase todo atributo de um controle é uma propriedade observável: textProperty(), disableProperty(), visibleProperty(). Uma propriedade guarda um valor e avisa quem estiver interessado quando ele muda. Sobre isso se constroem os bindings:

botao.disableProperty().bind(campo.textProperty().isEmpty());

Essa linha não "desliga o botão agora". Ela declara uma regra permanente: o botão está desligado sempre que o campo estiver vazio. Digite uma letra e o botão liga; apague e ele desliga. Não há evento tratado nem if espalhado. A regra é mantida pelo próprio JavaFX, como uma fórmula de planilha que se recalcula quando uma célula da qual depende muda.

As coleções seguem a mesma ideia. Uma ObservableList avisa quando itens entram ou saem, e sobre ela se empilham vistas: a FilteredList mostra só os itens que passam num predicado, e a SortedList os ordena por um comparador. Mudou a lista original? As vistas se atualizam. Mudou o predicado? A vista filtrada se refaz. É a mesma arquitetura do TableRowSorter do Swing, mas genérica e composta.

A estrutura: FXML

O FXML descreve a árvore de nós em XML. Sem uma linha de lógica, ele diz o que existe e onde:

<?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.ProgressIndicator?>
<?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?>

<!-- A estrutura da tela: nenhuma linha de lógica aqui. O fx:controller liga este arquivo à classe Java. -->
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
            fx:controller="br.com.exemplo.catalogo.CatalogoControlador"
            styleClass="raiz">
    <top>
        <HBox spacing="8" styleClass="barra">
            <Label text="Buscar:"/>
            <TextField fx:id="busca" promptText="nome da trilha" prefColumnCount="20"/>
            <Button fx:id="recarregar" text="Recarregar" onAction="#recarregar"/>
            <ProgressIndicator fx:id="carregando" prefHeight="20" prefWidth="20"/>
        </HBox>
    </top>
    <center>
        <TableView fx:id="tabela">
            <columns>
                <TableColumn fx:id="colNome" text="Nome" prefWidth="200"/>
                <TableColumn fx:id="colRegiao" text="Região" prefWidth="100"/>
                <TableColumn fx:id="colDistancia" text="Distância (km)" prefWidth="120" styleClass="numero"/>
                <TableColumn fx:id="colDificuldade" text="Dificuldade" prefWidth="120"/>
            </columns>
            <placeholder><Label text="Nenhuma trilha para mostrar."/></placeholder>
        </TableView>
    </center>
    <bottom>
        <Label fx:id="status" styleClass="status">
            <padding><Insets top="4" right="8" bottom="4" left="8"/></padding>
        </Label>
    </bottom>
</BorderPane>

Cada elemento XML cria um objeto (<Button> chama new Button()), e cada atributo chama um setter (text="Recarregar" chama setText("Recarregar")). O fx:id dá nome a um nó para que o controlador o receba. O onAction="#recarregar" liga o clique a um método do controlador. E o fx:controller diz qual classe é o controlador.

Por que separar isso do Java? Porque estrutura, aparência e comportamento mudam por motivos diferentes e, muitas vezes, por pessoas diferentes. O arquivo FXML pode ser editado visualmente no Scene Builder, a ferramenta de arrastar e soltar mantida pela Gluon, sem tocar no código. E a revisão de código de uma mudança de layout mostra só o XML que mudou.

A aparência: CSS

/* A aparência mora aqui, separada da estrutura (FXML) e do comportamento (Java). */
.raiz {
    -fx-font-family: "DejaVu Sans";
    -fx-font-size: 13px;
    -verde-mata: #2e7d4f;
}
.barra {
    -fx-padding: 8;
    -fx-alignment: center-left;
    -fx-background-color: linear-gradient(to bottom, #f4f9f5, #e3efe6);
}
.button {
    -fx-background-color: -verde-mata;
    -fx-text-fill: white;
    -fx-background-radius: 4;
}
.button:hover { -fx-background-color: derive(-verde-mata, 20%); }
.table-view .column-header .label { -fx-font-weight: bold; }
.numero { -fx-alignment: center-right; }
.status { -fx-text-fill: #555; }

O CSS do JavaFX parece o da web, mas com diferenças importantes. As propriedades têm o prefixo -fx-, e os nomes das classes dos controles vêm do próprio JavaFX (.button, .table-view, .column-header). O -verde-mata é uma cor nomeada (looked-up color): declarada uma vez na raiz, herdada por toda a árvore, e combinada com funções como derive(cor, 20%), que clareia a cor em 20% para o efeito de hover. Trocar o tema do programa inteiro passa a ser trocar uma linha.

A styleClass="numero" numa TableColumn vale também para as células daquela coluna. É assim que a distância fica alinhada à direita, sem código.

O comportamento: o controlador

package br.com.exemplo.catalogo;

import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;

import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** O comportamento da tela. Os campos @FXML são preenchidos pelo FXMLLoader a partir dos fx:id. */
public class CatalogoControlador {

    @FXML private TextField busca;
    @FXML private Button recarregar;
    @FXML private ProgressIndicator carregando;
    @FXML private TableView<Trilha> tabela;
    @FXML private TableColumn<Trilha, String> colNome;
    @FXML private TableColumn<Trilha, String> colRegiao;
    @FXML private TableColumn<Trilha, Double> colDistancia;
    @FXML private TableColumn<Trilha, String> colDificuldade;
    @FXML private Label status;

    private final FonteDeTrilhas fonte;
    private final ExecutorService trabalho = Executors.newVirtualThreadPerTaskExecutor();
    private final ObservableList<Trilha> trilhas = FXCollections.observableArrayList();
    private final FilteredList<Trilha> filtradas = new FilteredList<>(trilhas, _ -> true);

    /** Recebe a dependência pelo construtor: quem cria o controlador é a "controller factory". */
    public CatalogoControlador(FonteDeTrilhas fonte) {
        this.fonte = fonte;
    }

    /** Chamado pelo FXMLLoader DEPOIS de preencher os campos @FXML. */
    @FXML
    private void initialize() {
        // Cada coluna diz como extrair seu valor de um Trilha (records não têm getNome(), então usamos lambdas)
        colNome.setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().nome()));
        colRegiao.setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().regiao()));
        colDistancia.setCellValueFactory(c -> new ReadOnlyObjectWrapper<>(c.getValue().distanciaKm()));
        colDificuldade.setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().dificuldade()));
        colDistancia.setCellFactory(_ -> new javafx.scene.control.TableCell<>() {
            @Override protected void updateItem(Double km, boolean vazio) {
                super.updateItem(km, vazio);
                setText(vazio || km == null ? null : String.format(Locale.of("pt", "BR"), "%.1f", km));
            }
        });

        // lista original → filtrada pela busca → ordenada pelo cabeçalho da tabela
        var ordenadas = new SortedList<>(filtradas);
        ordenadas.comparatorProperty().bind(tabela.comparatorProperty());
        tabela.setItems(ordenadas);

        // O filtro "escuta" o campo de busca: nenhum botão, nenhum evento de teclado tratado à mão
        busca.textProperty().addListener((_, _, texto) -> {
            String t = texto.strip().toLowerCase(Locale.ROOT);
            filtradas.setPredicate(trilha -> t.isEmpty() || trilha.nome().toLowerCase(Locale.ROOT).contains(t));
        });

        // O rodapé é uma EXPRESSÃO sobre as listas: recalcula sozinho quando elas mudam
        status.textProperty().bind(Bindings.createStringBinding(
                () -> filtradas.size() + " de " + trilhas.size() + " trilhas", filtradas, trilhas));

        carregando.setVisible(false);
        recarregar();
    }

    @FXML
    private void recarregar() {
        Task<List<Trilha>> tarefa = new Task<>() {
            @Override protected List<Trilha> call() throws Exception {
                return fonte.buscarTodas();                     // fora da thread da interface
            }
        };
        // Enquanto a tarefa roda: indicador visível, botão desligado. Tudo por binding.
        carregando.visibleProperty().bind(tarefa.runningProperty());
        recarregar.disableProperty().bind(tarefa.runningProperty());
        tarefa.setOnSucceeded(_ -> trilhas.setAll(tarefa.getValue()));   // de volta na thread da interface
        tarefa.setOnFailed(_ -> {
            status.textProperty().unbind();
            status.setText("Falhou: " + tarefa.getException().getMessage());
        });
        trabalho.submit(tarefa);
    }

    public void encerrar() {
        trabalho.shutdownNow();
    }
}

Compare cada decisão com a versão em Swing.

O filtro é um predicado sobre uma vista. Uma linha de listener no textProperty() troca o predicado da FilteredList, e a tabela, que mostra a SortedList construída sobre ela, se atualiza. A ordenação ao clicar no cabeçalho vem de uma linha: o comparador da SortedList é ligado ao comparador da tabela. Não houve modelo de tabela escrito à mão.

O rodapé é uma fórmula. Bindings.createStringBinding recebe a expressão e as dependências (as duas listas). Sempre que uma delas muda, o texto é recalculado. Na versão Swing, era preciso lembrar de chamar a atualização do rodapé em cada lugar que alterasse os dados. Aqui, não há como esquecer.

O estado de "carregando" também é uma fórmula. A Task tem uma propriedade runningProperty(), verdadeira enquanto ela executa. O indicador fica visível e o botão fica desligado exatamente enquanto ela for verdadeira, e nenhuma linha de código liga ou desliga nada. Compare com os setVisible e setEnabled espalhados pelo carregar() e pelo finally do Swing.

Colunas com records. Muitos tutoriais usam new PropertyValueFactory<>("nome"), que procura um método getNome() ou nomeProperty() por reflexão. Records não têm getNome(), só nome(), e a coluna fica vazia sem exceção nenhuma. O único sinal é um aviso no log, "Can not retrieve property 'nome' in PropertyValueFactory", fácil de perder no meio dos outros. A lambda c -> new ReadOnlyStringWrapper(c.getValue().nome()) é verificada pelo compilador: se o componente do record mudar de nome, o erro aparece na compilação, e não como uma coluna misteriosamente em branco.

Uma célula sob medida. A setCellFactory da distância cria células que formatam o número em português, com vírgula. A tabela reaproveita as células ao rolar (só existem as visíveis), e por isso o updateItem precisa tratar o caso vazio: uma célula reciclada pode ter mostrado outra trilha antes.

A Task e as threads

A Task é o SwingWorker do JavaFX, e o respeito à mesma regra de ouro. O call() roda na thread em que você submeter a tarefa (aqui, uma virtual thread). Os tratadores setOnSucceeded e setOnFailed rodam de volta na JavaFX Application Thread, onde é seguro mexer na interface. A própria Task é uma fonte de propriedades observáveis: runningProperty, progressProperty e messageProperty podem ser atualizadas de dentro do call() com updateProgress e updateMessage, e chegam à tela na thread certa.

E se alguém esquecer a regra e mexer na tela a partir de outra thread? O JavaFX confere parte das operações. Rodei três tentativas a partir de uma thread comum, com a janela visível:

rotulo.setText(...) → não reclamou
caixa.getChildren().add(...) → IllegalStateException: Not on FX application thread; currentThread = Thread-2
new Stage().show() → IllegalStateException: Not on FX application thread; currentThread = Thread-2
texto final: mudado fora da thread

A estrutura da árvore (acrescentar filhos) e as janelas são protegidas: a violação é detectada na hora, com uma mensagem clara. O setText é mais traiçoeiro. A chamada retornou normalmente, e a propriedade mudou. Mas o console mostrou, logo acima, a mesma IllegalStateException lançada dentro do skin do rótulo, o código que redesenha o controle, ao reagir à mudança. O valor e a tela ficam, por um instante, em desacordo, e o programa segue como se nada tivesse acontecido. É o pior tipo de bug: às vezes funciona. A regra não muda: fora da thread da interface, use Platform.runLater(() -> ...) ou deixe a Task entregar o resultado.

Ligando tudo: a aplicação e a fábrica de controladores

package br.com.exemplo.catalogo;

public record Trilha(String nome, String regiao, double distanciaKm, String dificuldade) {}
package br.com.exemplo.catalogo;

import java.util.List;

/** De onde vêm as trilhas. Hoje, uma lista fixa; na aula do app MVVM, a API REST. */
public interface FonteDeTrilhas {
    List<Trilha> buscarTodas() throws Exception;

    static FonteDeTrilhas deExemplo() {
        return () -> {
            Thread.sleep(800);                               // finge a latência da rede
            return List.of(
                    new Trilha("Lagoinha do Leste", "Sul", 4.5, "Moderada"),
                    new Trilha("Costão do Santinho", "Norte", 3.2, "Fácil"),
                    new Trilha("Morro da Coroa", "Leste", 6.8, "Difícil"),
                    new Trilha("Praia do Saquinho", "Sul", 2.1, "Fácil"),
                    new Trilha("lagoa Pequena", "Leste", 1.2, "Fácil"));
        };
    }
}
package br.com.exemplo.catalogo;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import javax.imageio.ImageIO;
import java.io.File;

public class CatalogoApp extends Application {

    private CatalogoControlador controlador;

    @Override
    public void start(Stage palco) throws Exception {
        var carregador = new FXMLLoader(CatalogoApp.class.getResource("catalogo.fxml"));
        // Quem cria o controlador somos nós: é aqui que entram as dependências
        carregador.setControllerFactory(tipo -> {
            if (tipo == CatalogoControlador.class) return new CatalogoControlador(FonteDeTrilhas.deExemplo());
            throw new IllegalArgumentException("controlador desconhecido: " + tipo);
        });
        Parent raiz = carregador.load();
        controlador = carregador.getController();

        var cena = new Scene(raiz, 640, 320);
        cena.getStylesheets().add(CatalogoApp.class.getResource("catalogo.css").toExternalForm());
        palco.setTitle("Trilhas da Ilha — Catálogo");
        palco.setScene(cena);
        palco.show();

        String foto = System.getenv("FOTO");                     // só para o teste automatizado
        if (foto != null) fotografarDepois(cena, foto);
    }

    @Override
    public void stop() {
        controlador.encerrar();                                   // a janela fechou: libera as threads
    }

    private void fotografarDepois(Scene cena, String arquivo) {
        Thread.ofVirtual().start(() -> {
            try {
                Thread.sleep(2000);
                Platform.runLater(() -> ((javafx.scene.control.TextField) cena.lookup("#busca")).setText("lago"));
                Thread.sleep(500);
                Platform.runLater(() -> {
                    try {
                        ImageIO.write(SwingFXUtils.fromFXImage(cena.snapshot(null), null), "png", new File(arquivo));
                        System.out.println("status: " + ((javafx.scene.control.Label) cena.lookup("#status")).getText());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    Platform.exit();
                });
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }

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

A fábrica de controladores é a peça que liga esta aula a "Extensão — Spring Boot pela Primeira Vez: Injeção de Dependência". Sem ela, o FXMLLoader cria o controlador com o construtor sem argumentos, e o controlador teria de buscar a sua fonte de dados sozinho, com um new escondido, um singleton ou um estático, o que o tornaria impossível de testar com um dublê. Com a fábrica, nós criamos o controlador e lhe entregamos as dependências pelo construtor. Numa aplicação maior, a fábrica pode simplesmente perguntar a um contêiner de DI: carregador.setControllerFactory(contexto::getBean).

O fotografarDepois existe apenas para testes automatizados, e é ele o motivo do requires javafx.swing no module-info.java e da dependência javafx-swing: SwingFXUtils e ImageIO ficam fora do javafx.controls, e sem essa linha a compilação falha com "package javafx.embed.swing is not visible". Com a variável FOTO definida, o programa digita "lago" na busca, tira um snapshot da cena e sai. No teste, a imagem mostrou a barra com o fundo em degradê verde-claro, o botão "Recarregar" verde, a tabela com cabeçalhos em negrito, as duas trilhas filtradas com a distância em formato brasileiro, "4,5" e "1,2", alinhada à direita, e o rodapé:

status: 2 de 5 trilhas

O método stop() fecha o executor quando a janela fecha. Esquecê-lo é um erro comum: threads de trabalho que não são daemon podem impedir a JVM de terminar, e o programa "fecha" a janela mas continua rodando. As virtual threads são daemon por definição, mas o hábito de liberar o que você abriu vale para qualquer executor.

O que o JavaFX oferece além disso

Esta aula cobriu o núcleo, mas a plataforma vai bem além:

  • Gráficos prontos (LineChart, BarChart, PieChart), que se atualizam quando a ObservableList de dados muda.
  • Animações (Timeline, FadeTransition, TranslateTransition), que animam propriedades.
  • Formas e efeitos (sombras, desfoque, reflexos) aplicados a qualquer nó.
  • Um navegador embutido, o WebView, com WebKit, e mídia, com o MediaPlayer.
  • Controles de texto rico, com o RichTextArea, em evolução nas versões recentes.

Para distribuição, o javafx:jlink do plugin Maven gera um runtime com a aplicação, e o jpackage o transforma num instalador nativo, a mesma trilha de "Do JAR ao Executável — jlink, jpackage e GraalVM Native Image". A Gluon mantém ainda ferramentas para levar aplicações JavaFX ao Android e ao iOS, compiladas com o GraalVM Native Image.

A janela está bonita e se comporta bem, mas ainda mente: as trilhas vêm de uma lista fixa no código. O controlador também acumula responsabilidades demais, e é difícil testar sem abrir uma janela. Tirar do controlador todo o estado e toda a lógica e colocá-los num objeto feito só de propriedades, testável com JUnit sem tela nenhuma, é a ideia da arquitetura MVVM.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um colega configurou a coluna de nome com colNome.setCellValueFactory(new PropertyValueFactory<>("nome")). O programa compila e roda sem erros, mas a coluna aparece vazia. Explique e corrija.

Ver resposta

✓ Resposta: O PropertyValueFactory procura, por reflexão, um método nomeProperty() e, se não achar, um getNome(). O record Trilha tem apenas o acessor nome(), sem o prefixo get, e por isso a fábrica não encontra nada e devolve null para cada célula, sem lançar exceção. Só sai um aviso no log ("Can not retrieve property 'nome' in PropertyValueFactory"), com a causa "Cannot read from unreadable property nome". A correção é a lambda tipada da aula: colNome.setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().nome())), que o compilador verifica. Se o componente do record mudar de nome, o erro aparece na compilação.

Exercício 2

Acrescente um botão "Limpar" que apaga a busca e que fica desligado quando a busca já está vazia, sem escrever nenhum if.

Ver resposta

✓ Resposta: No FXML: <Button fx:id="limpar" text="Limpar" onAction="#limpar"/>. No controlador, @FXML private Button limpar;, a linha limpar.disableProperty().bind(busca.textProperty().isEmpty()); dentro do initialize(), e o método @FXML private void limpar() { busca.clear(); }. O binding mantém a regra "desligado sempre que a busca estiver vazia" para sempre, inclusive quando a busca é apagada pelo próprio botão, sem código de estado.

Exercício 3

No teste da aula, o setText feito fora da thread "não reclamou", mas o getChildren().add lançou exceção. Por que essa diferença é perigosa, e como tornar o código correto?

Ver resposta

✓ Resposta: O JavaFX verifica a thread em operações estruturais (alterar filhos, abrir janelas), mas não na atribuição de todas as propriedades. No setText, o valor muda e o erro só acontece dentro do skin, que reage à mudança para redesenhar o rótulo. A exceção vai para o log e o chamador segue achando que deu certo. O resultado é um erro intermitente e silencioso, em que a tela às vezes não reflete o valor, muito mais difícil de encontrar que uma exceção imediata. O código correto faz toda alteração de interface na JavaFX Application Thread: Platform.runLater(() -> rotulo.setText("...")), ou, melhor ainda, entrega o resultado por uma Task e seus tratadores setOnSucceeded e setOnFailed.

Exercício 4

Mostre como fazer a Task do recarregar() exibir no rodapé mensagens como "Conectando…" e "Recebendo 5 trilhas…" enquanto roda, voltando ao "N de M trilhas" no fim.

Ver resposta

✓ Resposta: Dentro do call(), chame updateMessage("Conectando…") antes da busca e updateMessage("Recebendo " + lista.size() + " trilhas…") depois dela. Esses métodos podem ser chamados de qualquer thread e chegam à tela na thread certa. Antes de submeter a tarefa, troque o binding do rodapé por um condicional: status.textProperty().bind(Bindings.when(tarefa.runningProperty()).then(tarefa.messageProperty()).otherwise(resumo)), em que resumo é o createStringBinding com as listas, guardado num campo. Enquanto a tarefa roda, o rodapé mostra a mensagem dela; quando termina, volta automaticamente ao resumo.

Exercício 5

Por que injetar a FonteDeTrilhas pelo construtor do controlador, com a fábrica de controladores, e não com new FonteDeTrilhas... dentro do próprio controlador? Dê dois benefícios concretos.

Ver resposta

✓ Resposta: O primeiro benefício é a testabilidade. Um teste pode criar o controlador com uma fonte falsa que devolve dados fixos, ou que lança uma exceção para exercitar o caminho de erro, sem rede e sem API no ar. Com um new interno, o controlador sempre usaria a fonte real. O segundo é a troca sem mexer no controlador. A mesma tela pode mostrar dados de exemplo, da API de desenvolvimento ou da de produção, conforme o que a aplicação entrega na fábrica, e numa aplicação maior a fábrica pode delegar a um contêiner de injeção de dependência (contexto::getBean). É o mesmo princípio de "Extensão — Spring Boot pela Primeira Vez: Injeção de Dependência": quem usa uma dependência não deve ser quem a constrói.

Comentários

Mais em Java

Gravidade de Verdade — Física com Box2D
Gravidade de Verdade — Física com Box2D

Nesta aula você aprende a usar o Box2D pelo libGDX: mundo, corpos, formas…

O Programa que se Examina — Anotações e Reflexão
O Programa que se Examina — Anotações e Reflexão

Como declarar anotações próprias em Java, escolher a retenção certa e usar a…

Moldes e Instâncias — Classes, Objetos e Construtores
Moldes e Instâncias — Classes, Objetos e Construtores

Classes e objetos em Java: campos, new e referências, o heap, null e a…