DEV Community

Cover image for Uma introdução ao Hilla
Diego Cardoso
Diego Cardoso

Posted on

Uma introdução ao Hilla

Este é o meu primeiro post nesta plataforma e pra marcar esta estreia, gostaria de escrever sobre um novo framework em Java desenvolvido pela empresa que trabalho: o Hilla.

O que é Hilla?

Nas palavras do próprio site oficial do framework, "Hilla integra um back end em Spring Boot com um front end reativo em TypeScript".

Hilla surgiu como uma alternativa ao framework fullstack da Vaadin, o Flow. Anteriormente chamado de Fusion, decidiu-se que era o momento para uma nova cara e posicionamento da marca, para torná-la mais visível aos desenvolvedores que antes poderiam ficar confusos em haver duas opções de desenvolvimento dentro da mesma ferramenta.

Ainda é possível que uma aplicação rode em modo híbrido (com páginas em Hilla e Flow), mas isso é mais indicado para aplicações já existentes que desejem transicionar de um modelo para o outro.

Hilla é uma palavra finlandesa dado para uma fruta chamada amora-branca-silvestre, bastante comum na região onde fica a sede da Vaadin.

Mas como isso funciona?

Em essência, Hilla permite que o desenvolvedor crie endpoints no back end em Java que são então usados pelo framework para gerar classes e funções em TypeScript que podem ser usados para comunicação do front end com o back end. Como esta criação dos endpoints no cliente é feita de forma automática pelo framework, o desenvolvedor tem a possibilidade de ser informado de eventuais erros de maneira mais rápida.

Além da geração dos endpoints, Hilla se encarrega de criar todos os tipos dos modelos criados em Java para TypeScript. Um outro ponto a se destacar é que Hilla utiliza-se da biblioteca Lit para criar as views no front end. Lit é uma biblioteca desenvolvida pelo Google para criação de custom components definidos pelos padrões da web. A escolha de uma biblioteca no front end faz com que Hilla consiga prover algumas ferramentas de suporte na criação de aplicativos, como por exemplo, componentes, lógica para validação de formulários, criação de rotas, entre outros.

Criando uma aplicação em Hilla

Para se criar e executar uma aplicação em Hilla, você precisará ter instalados em seu ambiente:

  • Node 16.14 ou mais novo
  • JDK 11 ou mais novo

Para criar um novo projeto, você pode utilizar o CLI da Vaadin rodando o comando:

npx @vaadin/cli init --hilla my-hilla-project
Enter fullscreen mode Exit fullscreen mode

Entre na pasta recém-criada do projeto e inicie a aplicação executando o Maven wrapper incluso:

cd my-hilla-project
./mvnw
Enter fullscreen mode Exit fullscreen mode

Após o projeto ser inicializado e o front end bundler terminar de ser executado, você verá uma página igual a essa:

Projeto Hilla rodando pela primeira vez

Pronto! Agora estamos preparados para botar a mão na massa.

Estrutura do projeto

Existem duas pastas principais em um projeto Hilla, /src e /frontend e serão nelas que iremos trabalhar:

  • Na pasta /src, está contida a parte do projeto que será executada no servidor. É onde encontraremos todo o nosso código em Java e também alguns recursos, como imagens e ícones.
  • Como o próprio nome sugere, a pasta /frontend contém o código que será executado no cliente, como as views e também os arquivos de estilo em CSS.

Criando o primeiro endpoint

Como todo bom tutorial, a gente vai fazer um simples gerenciador de tarefas. Vamos começar criando a classe de modelo Todo em /src/main/java/com/example/models/Todo.java:

package com.example.application.models;

import javax.validation.constraints.NotBlank;

public class Todo {
    private Integer id;
    @NotBlank
    private String task;
    private boolean done;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTask() {
        return task;
    }

    public void setTask(String task) {
        this.task = task;
    }

    public boolean isDone() {
        return done;
    }

    public void setDone(boolean done) {
        this.done = done;
    }
}

Enter fullscreen mode Exit fullscreen mode

Agora, iremos criar a classe que abrigará o nosso primeiro endpoint TodoService em /src/main/java/com/example/services/TodoService.java:

package com.example.services;

import java.util.ArrayList;
import java.util.List;

import com.example.models.Todo;
import com.vaadin.flow.server.auth.AnonymousAllowed;

import dev.hilla.Endpoint;
import dev.hilla.Nonnull;

@Endpoint
@AnonymousAllowed
public class TodoService {
    private final List<Todo> todoList = new ArrayList<>();

    public @Nonnull List<@Nonnull Todo> getTodos() {
        return todoList;
    }

    public @NonNull Todo save(@NonNull Todo todo) {
        todo.setId(todoList.size());
        todoList.add(todo);
        return todo;
    }
}

Enter fullscreen mode Exit fullscreen mode

Vamos agora dissecar um pouco o código que acabamos de criar.

A classe Todo é um simples JavaBean com alguns setters e getters. Note que podemos adicionar alguns validadores, como @NotBlank, às propriedades da classe.

Na classe TodoService é onde a maior parte da magia acontece:

  • Primeiro, anotamos a classe como @Endpoint. Isso é importante para que a classe seja usada pelo framework para gerar a sua contraparte no front end em TypeScript.
  • A seguir, encontramos a anotação @AnonymousAllowed. Por padrão, todos os endpoints necessitam de autenticação para serem usados. Esta anotação faz com que qualquer usuário consiga acessar aos serviços dessa classe.
  • Dentro da classe, definimos os métodos que serão expostos no cliente: primeiro um método para recuperar a lista de tarefas e um outro para salvar uma nova tarefa. Note que usamos a anotação @Nonnull em getTodos e em save. Isto faz com que o gerador não use undefined como um possível valor de retorno dos métodos.

Após, criarmos e salvarmos estas classes, você irá notar (se o servidor ainda estiver rodando ou na próxima vez que ele for inicializado) que algo de novo apareceu dentro da pasta /frontend/generated:

Arquivos gerados pelo Hilla

Esses arquivos correspondem ao serviço e o modelo criados anteriormente:

  • Todo.ts é uma interface que contém as mesmas propriedades definidas em Todo.java
  • TodoModel.ts é uma classe extendendo ObjectModel e é basicamente usada para validação e vinculação das propriedades de Todo.ts com componentes de formulário.
  • TodoService.ts define e exporta as funções correspondente aos endpoints definidos em TodoService.java

Criando a página de Todos

Agora que temos os endpoints criados, resta-nos criar a página para adicionar e visualizar as tarefas. Vamos criar um novo arquivo em /frontend/views/todo/todo-view.ts:

import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { View } from '../view';

@customElement('todo-view') // 1
export class TodoView extends View { // 2
    render() { // 3
        return html` // 4
            <h1>Minhas tarefas</h1>
        `;
    }
}
Enter fullscreen mode Exit fullscreen mode

Como falado anteriormente, Hilla utiliza a biblioteca Lit para criar as views no seu projeto. Vamos ver o que acabamos de criar:

  1. @customElement('todo-view') - Como mencionado, Lit é uma biblioteca para criação de custom elements. Essa anotação serve para marcar a classe como um custom element ao mesmo tempo que define o nome do elemento criado (obrigatoriamente, o nome deve conter pelo menos um traço - para evitar colisões com as tags padrões de HTML).
  2. export class TodoView extends View - cria um classe TodoView extendendo a classe View provida pelo próprio projeto. Normalmente, estenderíamos diretamente da classe LitElement, mas usando View temos algumas vantagens, como suporte a MobX, além de desabilitar o shadow root do custom element criado.
  3. render() - método invocado pelo Lit no momento em que uma instância do componente for renderizado na página.
  4. return html - aqui definiremos o markup da instância do nosso component. No momento, o componente irá adicionar um <h1> com o texto "Minhas tarefas" na tela.

Por enquanto, não podemos ver a nossa nova página funcionando, porque ainda não definimos nenhuma rota para ela. Para isso, basta que alteremos o arquivo em /frontend/routes.ts e adicionar uma nova entrada no array views:

import './views/todo/todo-view';

// ...

export const views: ViewRoute[] = [

  // ...  
  },
  {
    path: 'todo',
    component: 'todo-view',
    title: 'Tarefas',
  },
];
Enter fullscreen mode Exit fullscreen mode

E, pronto! Temos a nossa página funcionando...

Nova página "Minhas tarefas" funcionando!

... bem, mais ou menos. Ainda não temos como adicionar as nossas tarefas 😫.

Dando vida à nossa lista de tarefas

Agora podemos adicionar nosso pequeno formulário e tabela para adicionar e visualizar as nossas tarefas. Os componentes que iremos utilizar fazem parte do pacote do design system criado pela Vaadin e já utilizados por milhares de programadores em todo mundo. Vamos voltar ao nosso arquivo e inserir as funcionalidades restantes:

Primeiro, adicionaremos duas propriedades à class TodoView:

  @state()
  private todos: Todo[] = []; // 1

  private binder = new Binder(this, TodoModel); // 2
Enter fullscreen mode Exit fullscreen mode
  1. Um array com o nome todos que servirá para armazenar a lista de tarefas adicionadas pelo usuário. Note a marcação @state, que serve para fazer o Lit observar e reagir às alterações nesta propriedade. Além disso, marcamos a propriedade com o tipo gerado Todo.
  2. Instância de Binder (uma classe para manipulação de campos de formulário) que usaremos no formulário de criação de uma nova tarefa.

Com isso feito, vamos agora alterar o método render e adicionar o restante dos elementos da nossa página:

  render() {
    return html`
      <section class="p-m">
        <h1>Minhas tarefas</h1>

        <div theme="spacing" class="flex gap-s items-end">
          <vaadin-text-field
            label="Nova tarefa"
            ${field(this.binder.model.task)}
            placeholder="Comprar ovos, estudar hilla..."
            style="width: 300px;"
          ></vaadin-text-field> <!-- 1 -->
          <vaadin-button @click="${this.createTodo}" theme="primary">Adicionar</vaadin-button> <!-- 2 -->
        </div>
        <div class="todos">
          ${this.todos.length === 0
            ? html` <span>Nenhuma tarefa adicionada</span> `
            : this.todos.map(
                (todo) => html`
                  <div class="todo ${todo.done ? 'done' : ''}">
                    <vaadin-checkbox
                      ?checked="${todo.done}"
                      @checked-changed="${(e: CheckboxCheckedChangedEvent) => this.updateTodo(todo, e.detail.value)}"
                    ></vaadin-checkbox>  <!-- 4 -->
                    <span>${todo.task}</span>
                  </div>
                `
              )}  <!-- 3 -->
        </div>
      </section>
    `;
  }

Enter fullscreen mode Exit fullscreen mode
  1. Criamos o campo de texto com um placeholder e usamos a diretiva field para atrelar a propriedade task de Todo ao valor inserido pelo usuário no campo.
  2. Um botão, com um ouvinte de eventos (event listener) para o evento de clique do usuário. Adiante iremos mostrar a implementação do método createTodo.
  3. A lista de tarefas criadas. Primeiro verificamos se a lista de tarefas está vazia para apresentar uma mensagem e, em caso contrário, iteramos sobre a lista todos através do método map para retornar uma lista de tarefas. Para quem tem experiência com React, poderá ver uma certa semelhança aqui.
  4. Um campo checkbox para que o usuário marque/desmarque a tarefa como feita. Adicionamos um event listener que será chamado toda vez que o usuário alterar o valor do campo.

Agora, precisamos chamar o método no servidor que retorna a lista de tarefas criadas. Para isso, podemos usar o método de tempo de vida (lifecycle) connectedCallback. Este método é chamado toda vez que o componente é adicionado à página:

  async connectedCallback() {
    super.connectedCallback();
    this.todos = await getTodos(); // 1
  }
Enter fullscreen mode Exit fullscreen mode
  1. Como estamos usando await aqui, precisamos marcar o método com async. getTodos() é a função gerada no cliente que fará a chamada ao servidor do método TodoService#getTodos que criamos anteriormente.

Por último, adicionaremos os métodos referenciados no markup criado:

  async createTodo() {
    const todo = await this.binder.submitTo(save); // 1
    if (todo) {
      this.todos = [...this.todos, todo]; // 2 
      this.binder.clear(); // 3
    }
  }

  updateTodo(todo: Todo, done: boolean) {
    const updatedTodo = { ...todo, done };
    this.todos = this.todos.map((t) => (t.id === todo.id ? updatedTodo : t)); // 4
    save(updatedTodo); // 5
  }
Enter fullscreen mode Exit fullscreen mode
  1. Chamamos o método submitTo de binder que recebe um callback como parâmetro. Este método passará uma instância de Todo com o valor do campo de texto inserido pelo usuário na propriedade task.
  2. Alteramos o objeto this.todos para adicionar a nova tarefa retornada em (1).
  3. O método clear de binder limpa os campos controlados por ele através da diretiva field.
  4. Atualizamos o objeto this.todos como a tarefa com o seu novo status (feito/não feito).
  5. Por fim, chamamos a função save para persistir a alteração da tarefa no servidor.

Abaixo o arquivo todo-view.ts completo:

todo-view.ts
import { html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { View } from '../view';

import '@vaadin/text-field';
import '@vaadin/button';
import '@vaadin/checkbox';
import { Binder, field } from '@hilla/form';
import { CheckboxCheckedChangedEvent } from '@vaadin/checkbox';
import { getTodos, save } from 'Frontend/generated/TodoService';
import Todo from 'Frontend/generated/com/example/application/models/Todo';
import TodoModel from 'Frontend/generated/com/example/application/models/TodoModel';

@customElement('todo-view')
export class TodoView extends View {
  @state()
  private todos: Todo[] = [];

  private binder = new Binder(this, TodoModel);

  async connectedCallback() {
    super.connectedCallback();
    this.todos = await getTodos();
  }

  render() {
    return html`
      <section class="p-m">
        <h1>Minhas tarefas</h1>

        <div theme="spacing" class="flex gap-s items-end">
          <vaadin-text-field
            label="Nova tarefa"
            ${field(this.binder.model.task)}
            placeholder="Comprar ovos, estudar hilla..."
            style="width: 300px;"
          ></vaadin-text-field>
          <vaadin-button @click="${this.createTodo}" theme="primary">Adicionar</vaadin-button>
        </div>
        <div class="todos">
          ${this.todos.length === 0
            ? html` <span>Nenhuma tarefa adicionada</span> `
            : this.todos.map(
                (todo) => html`
                  <div class="todo ${todo.done ? 'done' : ''}">
                    <vaadin-checkbox
                      ?checked="${todo.done}"
                      @checked-changed="${(e: CheckboxCheckedChangedEvent) => this.updateTodo(todo, e.detail.value)}"
                    ></vaadin-checkbox>
                    <span>${todo.task}</span>
                  </div>
                `
              )}
        </div>
      </section>
    `;
  }

  async createTodo() {
    const todo = await this.binder.submitTo(save);
    if (todo) {
      this.todos = [...this.todos, todo];
      this.binder.clear();
    }
  }

  updateTodo(todo: Todo, done: boolean) {
    const updatedTodo = { ...todo, done };
    this.todos = this.todos.map((t) => (t.id === todo.id ? updatedTodo : t));
    save(updatedTodo);
  }
}

Enter fullscreen mode Exit fullscreen mode

... e pronto! Temos o nosso gerenciador de tarefas em perfeito funcionamento:

Lista de tarefas criadas

Considerações finais

O intuito deste artigo foi mostrar de uma forma simples o básico para se começar a criar uma aplicação em Hilla. Há muito mais a ser explorado que precisou ser deixado de fora deste artigo porque ele ficaria imenso e cansativo.

A página de documentação é bastante extensa e apresenta as demais funcionalidades do framework que o permitem ser uma escolha para diversos tipos de projetos. Infelizmente, só temos a versão dela em inglês.

Espero que a leitura tenha valido a pena e, por favor, qualquer feedback é mais que bem-vindo!

Até mais!

Top comments (0)