28 de ago de 2014

Threads e JavaFX

Uma das coisas mais comuns que fazemos em nossa aplicação é executar algo "pesado". Por exemplo, usualmente lemos arquivos gigantes, acessamos páginas com muita informação, aguardamos a resposta do servidor, etc. Imagine que você queria fazer isso em paralelo na sua aplicação JavaFX, mas se deparada com erros. Como devemos proceder? Nesse artigo vamos falar como executar ações em paralelo na nossa aplicação sem causar transtorno aos nossos usuários.

Um caso de uso

Imagine que temos uma aplicação que mostra o código de uma página HTML dentro de um campo de texto. Para pegar o código de uma página, temos que abrir uma conexão com uma URL, ler o InputStream e em seguida atualizar o campo de texto com o resultado. Vamos as nossas soluções.

Caso 1: Abrir URL ao clique do botão

Nesse primeiro exemplo, iremos abrir a URL quando o usuário clicar no botão, veja o código:

EventHandler cenario1 = e -> {
 try {
  txtResultado.setText(carregaPagina(txtUrl.getText()));
 } catch (Exception e1) {
  e1.printStackTrace();
 }
};


Bem, esse código funciona, mas como ele trava a execução, a aplicação fica travada quando isso acontece:



Na imagem não fica nítido, mas a aplicação fica com o botão como se estivesse apertado. Como resolver isso?

Caso 2: Abrir URL em uma thread separada

Essa solução é a mais óbvia para quem já tem um pouco de intimidade com Java. O que podemos fazer é simplesmente abrir a URL em uma thread separada e pronto! A operação de abrir a URL seria em paralelo e atualização do campo de texto só feita quando terminassemos de ler a URL, veja o código:

EventHandler cenario2 = e -> {
 Thread t = new Thread(() -> {
  try {
   txtResultado.setText(carregaPagina(txtUrl.getText()));
  } catch (Exception e1) {
   e1.printStackTrace();
  }
 });
 t.start();
};

Legal, mas ao clicar algumas vezes no botão teremos o seguinte erro:

java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-3

E agora?

Threads e JavaFX

O ponto é que como JavaFX é quem coordena a modificação da parte visual da aplicação, ele não deixa que coisas feitas em outra thread tente atuar diretamente na view, assim ele tem controle da atualização da tela. Claro que os desenvolvedores da API já sabiam que fazer coisas em paralelo eram normais, assim eles arrumaram uma forma de ajudar você a criar aplicações que tenham execução de tarefas pesadas e não travar a thread do JavaFX!

Platform.runLater

Com esse método estático da classe Platform, podemos infomar ao JavaFX uma thread que será executada sob o controle dele, assim não teríamos o erro que tivemos no caso 2. Veja o nosso caso do clique:

Massa, agora não temos erro! No entanto, notem que essa thread que criamos está sob o controle do JavaFX, mas mesmo assim ela trava o botão... Como resolver isso de vez?

Task
As tasks são a solução! Com elas podemos executar algo em paralelo e pegar o resultado depois para sim atualizar a nossa aplicação. O funcionamento é simples, vamos focar em três principais métodos:


  • call: É onde fazemos nossa tarefa pesada. Esse médoto o JavaFX não está cuidando, logo não devemos alterar nada da view aqui;
  • succeded: Quando há o sucesso na execução do método call, esse método é chamado e dele podemos pegar o resultado do call usando o método getValue;
  • failed: Esse método já é chamado quando temos uma exceção na execução do método call. Nele também podemos pegar a exceção lançada com o método getExeception.
Pronto! Isso é o suficiente para começar a usar a Task, mas notem que um ponto importante é que a Task contém um tipo genérico, dessa forma temos segurança nos tipos dos dados e evitamos espalhar "casting" pelo código. Enfim, vamos ao nosso exemplo do botão com uma Task do tipo String:

EventHandler cenario4 = e -> {
 carregando.setVisible(true);
 Task tarefaCargaPg = new Task() {

  @Override
  protected String call() throws Exception {
   return carregaPagina(txtUrl.getText());
  }

  @Override
  protected void succeeded() {
   String codigo = getValue();
   carregando.setVisible(false);
   txtResultado.setText(codigo);
  }
 };
 Thread t = new Thread(tarefaCargaPg);
 t.setDaemon(true);
 t.start();
};

Ótimo! Veja nossa aplicação final abaixo. Para deixar tudo mais legal, colocamos um ProgressIndicator, assim quando a página está sendo carregada, a opção de carregar nova página fica também desabilitada.


Conclusão

Mostramos como fazer atividades em paralelo no JavaFX não é um bicho de 7 cabeças.
O nosso código está no github!

3 comentários:

  1. Isso é uma coisa que nao gostei no JavaFX. Achei burocratico demais. No swing era so meter tudo numa thread e tacar-lhe o pau.

    ResponderExcluir
  2. Boa Tarde. Sei que o tópico é antigo, mas talvez você possa me ajudar.

    E se eu quiser ir mostrando em uma TextArea o resultado linha a linha da importação que você está fazendo? E não o resultado todo de uma vez.

    Esclarecendo:
    Eu vou executar um comando externo usando Runtime. E isso me retorna algumas linha, como por exemplo: 1) Conexão ok. 2)Usuario ok, etc etc etc.
    Eu queria ir mostrando isso conforme vai sendo me retornado.

    É possível no JAVAFX ?

    ResponderExcluir
    Respostas
    1. Sim, é só você colocar o seu método de processamento numa thread separada e toda vez que quiser atualizar o seu campo de texto com o status do processamento você chama assim:
      Platform.runLater(() -> meuCampoDeTexto.setText("Usuário OK"));

      Excluir