Nesse clima eu criei essa aplicação média com JavaFX que faz uso de umaAPI de transparência para mostrar dados das eleições.
Embora eu tenha achado a aplicação muito interessante, devo deixar claro que essa aplicação não tem como objetivo substituir o ótimo DivulgaCand, do TSE, mas sim divulgar o projeto do pessoal do transparencia.org e também demonstrar um pouco do que aprendemos aqui nesse blog através de uma "aplicação do mundo real".
Uma API para busca de dados das eleições
Conforme relatei em meu blog pessoal, eu já tive a honra de criar uma API REST para expor os dados das eleições. Esse ano o pessoal do transparencia.org fez uma API muito e mais abrangente do a que eu tinha feito. O mais legal é que haverá um hackthon esse fim de semana com a API e há muitos exemplos de código e "wrappers" para facilitar o uso da mesma! Nessa postagem iremos usar um só método do Wrapper Java criado pelo Josué Eduardo.
Instalando e usando o "wrapper"
A nossa aplicação utiliza Maven e Java 8. Clone o wrapper usando git e então entre no diretório da aplicação e use o comando mvn install para que ela fique disponível para outras apĺicações maven. Assim, podems adicionar o seguinte à nossa aplicação com o objetivo de acessar as classes da API java do transparência:
Agora você tem tudo para começar a codificar Java e usar os dados da eleição, mas precisa de uma chave da API. A chave pode ser adquirida seguindo as instruções no site do dev.transparencia.org.br.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<dependency> | |
<groupId>com.sensedia</groupId> | |
<artifactId>transparencia-java-sdk</artifactId> | |
<version>1.0</version> | |
</dependency> |
Veja um exemplo de uso do wrapper para listar todos candidatos ao governo de SP:
TransparenciaClient cli = new TransparenciaClient("{Sua chave}");
cli.getCandidatos("SP", "3", null, null).forEach(c -> {
System.out.printf("%s do partido %s para o cargo %s.\n",
c.getApelido(), c.getPartido(), c.getCargo());
Simples, não? Notem que no meu código eu deixei a minha chave, mas, por gentileza, não abusem ;) Conto com vocês! Deixei para facilitar para quem quer só executar a aplicação para testar.
A aplicação JavaFX
A nossa aplicação JavaFX é um projeto maven também. Você pode clonar o código e importar no netbeans. Cuidado com o repositório maven configurado no Netbeans!Ao abrir a aplicação no netbeans e esperar ele importar as coisas certinhas, e executar a mesma com F6, você deverá ver:
Agora, vamos explicar algumas partes da aplicação e colocar links para aprofundamento do tópico envolvido:
Estrutura da aplicação
A App usa FXML e os campos do FXML são injetados no controller. É lá no controller que pegamos os dadosOs estados
Os estados sob a imagem do Brasil são Label que estão em um grupo. Na inicialização da aplicação temos um for nesses labels para registrar um listener que irá modificar o estado selecionado. Ao clicar no estado também carregamos uma imagem e o nome dele.Os cargos
Os cargos são botões do tipo ToggleButton. Quando um novo botão do grupo é selecionado, mudamos o cargo selecionado. Cada cargo tem um código e o código é "armazenado" no botão usando o UserData.Binding no cargo e estado selecionado
O cargo e o estado atualmente selecionados são armanezados em suas respectivas propriedades e quando há a mudança dos valores dos mesmos, iremos atualizar os dados da tabela.A tabela e a busca de dados
Quando há a mudança do estado ou do cargo, disparamos a busca com os novos valores. Quando está acontecendo a busca, temos uma propriedade que indica que os dados estão carregando, assim disabilitamos a tabela, a seleção de cargos e mostramos um ProgressIndicator usando binding.Paginação de dados
A API de transparência não nos deixa saber quantos candidatos temos, assim a paginação dos dados fica dificil, mas de qualquer forma, fizemos uma paginação com infinitas páginas, se não houver dados naquela página, não há erro, mas é uma requisição perdida ao servidor... Ao mudar a paginação também pedimos dados para o servidor.Estilo
Embora a aplicação não tenha sido muito modificada, temos um pequeno CSS para o estilo de algumas coisas. Aceitamos PR se você manjar da parte visual :D--
Enfim, a paginação e a parte de rodar uma "task" no fundo não foram tratadas nesse blog, mas serão em breve. Veja o código completo para entender melhor:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.jugvale.transparenciaexplorerfx; | |
import com.sensedia.transparencia.client.core.TransparenciaClient; | |
import com.sensedia.transparencia.client.ex.RestException; | |
import com.sensedia.transparencia.client.resources.Candidato; | |
import java.net.URL; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.ResourceBundle; | |
import java.util.logging.Level; | |
import java.util.logging.Logger; | |
import javafx.beans.property.BooleanProperty; | |
import javafx.beans.property.SimpleBooleanProperty; | |
import javafx.beans.property.SimpleStringProperty; | |
import javafx.beans.property.StringProperty; | |
import javafx.collections.FXCollections; | |
import javafx.concurrent.Task; | |
import javafx.fxml.FXML; | |
import javafx.fxml.Initializable; | |
import javafx.scene.Group; | |
import javafx.scene.control.Label; | |
import javafx.scene.control.Pagination; | |
import javafx.scene.control.ProgressIndicator; | |
import javafx.scene.control.TableColumn; | |
import javafx.scene.control.TableView; | |
import javafx.scene.control.ToggleButton; | |
import javafx.scene.control.ToggleGroup; | |
import javafx.scene.control.cell.PropertyValueFactory; | |
import javafx.scene.image.Image; | |
import javafx.scene.image.ImageView; | |
import javafx.scene.layout.Pane; | |
public class AppController implements Initializable { | |
TransparenciaClient cliente; | |
// Para a paginação | |
final int NUM_RES = 20; | |
final String ID_CARGO_GOVERNADOR = "3"; | |
final String ID_CARGO_SENADOR = "5"; | |
final String ID_CARGO_DEP_ESTADUAL = "7"; | |
final String ID_CARGO_DEP_FEDERAL = "6"; | |
final String ID_CARGO_DISTRITAL = "8"; | |
// TODO carregar de arquivo | |
final String CHAVE = "lGkuSLiphXo7"; | |
final String PRIMEIRO_ESTADO = "DF"; | |
// Um cache bem simples para os dados dos candidatos... | |
// Pode crescer muito e dar estouro de memória na aplicação | |
Map<String, List<Candidato>> cache; | |
private StringProperty estadoSelecionado; | |
private StringProperty cargoSelecionado; | |
private BooleanProperty carregando; | |
private BooleanProperty erro; | |
@FXML | |
private TableView<Candidato> tblCandidatos; | |
@FXML | |
private ProgressIndicator prgCarregando; | |
@FXML | |
Label lblErroBusca; | |
@FXML | |
private Pane pnlPaginador; | |
// Ainda não tem no meu SceneBuild | |
private Pagination paginador; | |
@FXML | |
private Group grpEstados; | |
@FXML | |
private Label lblNomeEstado; | |
@FXML | |
private TableColumn<Candidato, String> colApelido; | |
@FXML | |
private TableColumn<Candidato, String> colNome; | |
@FXML | |
private TableColumn<Candidato, String> colPartido; | |
@FXML | |
private TableColumn<Candidato, String> colNum; | |
@FXML | |
private ToggleGroup grpCargos; | |
@FXML | |
private ToggleButton btnGovernador; | |
@FXML | |
private ToggleButton btnSenador; | |
@FXML | |
private ToggleButton btnDepEstadual; | |
@FXML | |
private ToggleButton btnDepFederal; | |
@FXML | |
private ToggleButton btnDepDistrital; | |
@FXML | |
private ImageView imgEstado; | |
@Override | |
public void initialize(URL url, ResourceBundle rb) { | |
inicializaPropriedades(); | |
inicializaColunas(); | |
configuraCargos(); | |
configuraEstados(); | |
estadoSelecionado.set(PRIMEIRO_ESTADO); | |
} | |
private void inicializaPropriedades() { | |
cliente = new TransparenciaClient(CHAVE); | |
cache = new HashMap<>(); | |
estadoSelecionado = new SimpleStringProperty(); | |
cargoSelecionado = new SimpleStringProperty(); | |
carregando = new SimpleBooleanProperty(); | |
erro = new SimpleBooleanProperty(); | |
// a minha versão do SceneBuilder ainda não tem um paginador... | |
paginador = new Pagination(Pagination.INDETERMINATE); | |
paginador.setMinWidth(600); | |
pnlPaginador.getChildren().setAll(paginador); | |
cargoSelecionado.addListener((chg, v, n) -> { | |
paginador.setCurrentPageIndex(0); | |
carregaCandidatos(); | |
}); | |
estadoSelecionado.addListener((chg, v, n) -> { | |
paginador.setCurrentPageIndex(0); | |
novoEstadoSelecionado(); | |
}); | |
paginador.currentPageIndexProperty().addListener((chg, v, n) -> { | |
// BUG: Será chamado duas vezes a carga | |
carregaCandidatos(); | |
}); | |
lblErroBusca.visibleProperty().bind(erro); | |
prgCarregando.visibleProperty().bind(carregando); | |
tblCandidatos.disableProperty().bind(carregando.or(erro)); | |
paginador.disableProperty().bind(carregando.or(erro)); | |
} | |
private void inicializaColunas() { | |
colApelido.setCellValueFactory(new PropertyValueFactory<>("apelido")); | |
colNome.setCellValueFactory(new PropertyValueFactory<>("nome")); | |
colPartido.setCellValueFactory(new PropertyValueFactory<>("partido")); | |
colNum.setCellValueFactory(new PropertyValueFactory<>("numero")); | |
} | |
// TODO: Fazer cache | |
// TODO: adicionar uma tela que indica que os dados estão carregando para o usuário | |
private void carregaCandidatos() { | |
carregando.set(true); | |
erro.set(false); | |
new Thread(new Task<List<Candidato>>() { | |
@Override | |
protected List<Candidato> call() throws Exception { | |
String estado = estadoSelecionado.get(); | |
String cargo = cargoSelecionado.get(); | |
estado = estado == null ? PRIMEIRO_ESTADO : estado; | |
cargo = cargo == null ? ID_CARGO_GOVERNADOR : cargo; | |
return busca(estado, cargo); | |
} | |
@Override | |
protected void succeeded() { | |
List<Candidato> dadosLidos = this.getValue(); | |
tblCandidatos.setItems(FXCollections.observableArrayList(dadosLidos)); | |
carregando.set(false); | |
erro.set(false); | |
} | |
@Override | |
protected void failed() { | |
super.failed(); | |
Throwable ex = getException(); | |
Logger.getLogger(AppController.class.getName()).log(Level.SEVERE, null, ex); | |
carregando.set(false); | |
erro.set(true); | |
} | |
}).start(); | |
} | |
private List<Candidato> busca(String estado, String cargo) throws RestException { | |
// tenta ver se tem no cache, se não tiver busca novos e coloca no cache | |
int paginaAtual = paginador.getCurrentPageIndex(); | |
String chaveCache = estado + cargo + paginaAtual; | |
System.out.println(NUM_RES + " - " + NUM_RES * paginaAtual); | |
List<Candidato> res = cache.get(chaveCache); | |
if (res == null) { | |
// INFELIZMENTE a API não deixa a gente saber quantos candidatos tem.. | |
// Então eu começo com páginas de 50 candidatos... | |
//res = cliente.getCandidatosByCargo(estado, cargo); | |
res = cliente.getCandidatos(estado, null, null, cargo, NUM_RES, NUM_RES * paginaAtual); | |
cache.put(chaveCache, res); | |
} | |
return res; | |
} | |
private void configuraCargos() { | |
btnGovernador.setUserData(ID_CARGO_GOVERNADOR); | |
btnSenador.setUserData(ID_CARGO_SENADOR); | |
btnDepFederal.setUserData(ID_CARGO_DEP_FEDERAL); | |
btnDepEstadual.setUserData(ID_CARGO_DEP_ESTADUAL); | |
btnDepDistrital.setUserData(ID_CARGO_DISTRITAL); | |
grpCargos.selectToggle(btnGovernador); | |
grpCargos.selectedToggleProperty().addListener((obs, o, n) -> { | |
if (n != null) { | |
cargoSelecionado.set(n.getUserData().toString()); | |
} | |
}); | |
} | |
private void configuraEstados() { | |
grpEstados.getChildren().forEach(c -> { | |
Label l = (Label) c; | |
l.setOnMouseClicked(e -> estadoSelecionado.set(l.getText())); | |
}); | |
} | |
public void novoEstadoSelecionado() { | |
String siglaEstadoSelecionado = estadoSelecionado.get(); | |
String nomeEstado = EstadoUtil.nome(siglaEstadoSelecionado); | |
lblNomeEstado.setText(nomeEstado); | |
// TODO: Cache? | |
imgEstado.setImage(new Image(EstadoUtil.urlBandeira(siglaEstadoSelecionado))); | |
carregaCandidatos(); | |
} | |
} |
Próxima parte
Vamos voltar nesse blog com a segunda parte que é abrir uma telinha com detalhes de um candidato selecionado na tabela. Aceitaremos pull request!Conclusão
Apresentamos uma aplicação com a qual você pode aprender JavaFX e ainda exercer sua cidadania! Mais uma vez eu peço por gentileza para não abusarem da minha chave da API :/
O código inteiro está no github e gostaria de salientar que a aplicação inteira foi feita em algumas horas, digamos 5 horas corridas, mas isso por que a mesma teve uma mudança de planos e foi reescrita... Um agradecimento especial a minha namorada Luana que ficou programando e desenvolvendo a aplicação comigo em pleno fim de semana! :*
Nenhum comentário:
Postar um comentário