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
/frontendconté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
@NonnullemgetTodose emsave. Isto faz com que o gerador não useundefinedcomo 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 extendendoObjectModele é basicamente usada para validação e vinculação das propriedades deTodo.tscom componentes de formulário. -
TodoService.tsdefine 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 classeTodoViewextendendo a classeViewprovida pelo próprio projeto. Normalmente, estenderíamos diretamente da classeLitElement, mas usandoViewtemos algumas vantagens, como suporte a MobX, além de desabilitar o shadow root do custom element criado. -
render()- método invocado peloLitno 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
todosque 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
fieldpara atrelar a propriedadetaskdeTodoao 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
todosatravés do métodomappara 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
awaitaqui, precisamos marcar o método comasync.getTodos()é a função gerada no cliente que fará a chamada ao servidor do métodoTodoService#getTodosque 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
submitTodebinderque recebe um callback como parâmetro. Este método passará uma instância deTodocom o valor do campo de texto inserido pelo usuário na propriedadetask. - Alteramos o objeto
this.todospara adicionar a nova tarefa retornada em (1). - O método
cleardebinderlimpa os campos controlados por ele através da diretivafield. - Atualizamos o objeto
this.todoscomo a tarefa com o seu novo status (feito/não feito). - Por fim, chamamos a função
savepara 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!




Top comments (0)