Uma introdução ao Hilla
Tags: hilla, vaadin, java, typescript
Published: 2022-04-09 18:57:17

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](https://hilla.dev), "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](https://vaadin.com), 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](https://lit.dev) é uma biblioteca desenvolvida pelo Google para criação de _[custom components](https://developer.mozilla.org/pt-BR/docs/Web/Web_Components/Using_custom_elements)_ 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:

```ssh
npx @vaadin/cli init --hilla my-hilla-project
```

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

```ssh
cd my-hilla-project
./mvnw
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h8hvl8j3p51bh5unfpoa.png)
 
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`:

```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;
    }
}

```

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

```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;
    }
}

```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6u9a3s525hlah99jh3v.png)
 
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`:

```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>
        `;
    }
}
```

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](https://mobx.js.org/), 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`:

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

// ...

export const views: ViewRoute[] = [

  // ...  
  },
  {
    path: 'todo',
    component: 'todo-view',
    title: 'Tarefas',
  },
];
```

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

![Nova página "Minhas tarefas" funcionando!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ewy2qh4h6bq415z0eho3.png)

 ... 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`:

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

  private binder = new Binder(this, TodoModel); // 2
```

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:

```ts
  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>
    `;
  }

```
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:

```ts
  async connectedCallback() {
    super.connectedCallback();
    this.todos = await getTodos(); // 1
  }
```
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:

```ts
  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
  }
```

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:

{% spoiler todo-view.ts %}
```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);
  }
}

```
{% endspoiler %}

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

![Lista de tarefas criadas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vgqrxccsj8ti53q38wep.png)
 
## 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](https://hilla.dev/docs/) é 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!