Uma introdução ao Hilla
Diego Cardoso
Posted on April 9, 2022
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
Entre na pasta recém-criada do projeto e inicie a aplicação executando o Maven wrapper incluso:
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:
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;
}
}
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;
}
}
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
emgetTodos
e emsave
. Isto faz com que o gerador não useundefined
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
:
Esses arquivos correspondem ao serviço e o modelo criados anteriormente:
-
Todo.ts
é uma interface que contém as mesmas propriedades definidas emTodo.java
-
TodoModel.ts
é uma classe extendendoObjectModel
e é basicamente usada para validação e vinculação das propriedades deTodo.ts
com componentes de formulário. -
TodoService.ts
define e exporta as funções correspondente aos endpoints definidos emTodoService.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>
`;
}
}
Como falado anteriormente, Hilla utiliza a biblioteca Lit para criar as views no seu projeto. Vamos ver o que acabamos de criar:
-
@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). -
export class TodoView extends View
- cria um classeTodoView
extendendo a classeView
provida pelo próprio projeto. Normalmente, estenderíamos diretamente da classeLitElement
, mas usandoView
temos algumas vantagens, como suporte a MobX, além de desabilitar o shadow root do custom element criado. -
render()
- método invocado peloLit
no momento em que uma instância do componente for renderizado na página. -
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',
},
];
E, pronto! Temos a nossa página 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
- 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 geradoTodo
. - 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>
`;
}
- Criamos o campo de texto com um placeholder e usamos a diretiva
field
para atrelar a propriedadetask
deTodo
ao valor inserido pelo usuário no campo. - 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
. - 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étodomap
para retornar uma lista de tarefas. Para quem tem experiência com React, poderá ver uma certa semelhança aqui. - 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
}
- Como estamos usando
await
aqui, precisamos marcar o método comasync
.getTodos()
é a função gerada no cliente que fará a chamada ao servidor do métodoTodoService#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
}
- Chamamos o método
submitTo
debinder
que recebe um callback como parâmetro. Este método passará uma instância deTodo
com o valor do campo de texto inserido pelo usuário na propriedadetask
. - Alteramos o objeto
this.todos
para adicionar a nova tarefa retornada em (1). - O método
clear
debinder
limpa os campos controlados por ele através da diretivafield
. - Atualizamos o objeto
this.todos
como a tarefa com o seu novo status (feito/não feito). - 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);
}
}
... e pronto! Temos o nosso gerenciador de tarefas em perfeito funcionamento:
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!
Posted on April 9, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.