📚 Pré-requisitos Teóricos: este projeto consome os serviços da Parte 01 — Spring Boot API e a infraestrutura conteinerizada da Parte 05 — Docker + PostgreSQL.
Seja muito bem-vindo à Parte 06 do ecossistema de aplicações Lista de Tarefas!
Nesta etapa prática, você aprenderá a dominar a primeira das três grandes trilhas de equivalência entre desenvolvimento Web e Mobile Híbrido / Cross-platform:
\[\text{Web SPA (Vue.js 3)} \longrightarrow \text{Mobile Cross-Platform (Quasar Framework + Capacitor)}\]Enquanto nas Partes 03 e 04 construímos clientes nativos clássicos para desktop e mobile, a Parte 06 ensina a filosofia do Single Codebase: como reaproveitar a reatividade, os componentes e a lógica de uma aplicação Vue 3 para gerar simultaneamente uma Web SPA de alta performance e um aplicativo móvel nativo para Android e iOS utilizando o poderoso Quasar Framework.
Tanto a aplicação Web (Vue 3) quanto o cliente Mobile (Quasar) operam como consumidores da nossa Fonte Única da Verdade (SSOT): a API REST desenvolvida em Spring Boot na porta 8080, conectada ao banco de dados relacional (H2 ou PostgreSQL conteinerizado).
<script setup> com ref(), computed() e onMounted(), proporcionando código conciso e tipado.QLayout, QHeader, QList, QItem, QCheckbox, QBtn, QDialog, QInnerLoading).{ "id": 1, "descricao": "...", "concluida": false }.Um dos desafios fundamentais do desenvolvimento cross-platform é o roteamento dinâmico de rede:
http://localhost:8080/api/tarefas.Platform.is.capacitor && Platform.is.android e redireciona automaticamente para o endereço virtual do host:
\(\text{URL Mobile} = \mathbf{http://10.0.2.2:8080/api/tarefas}\)http://192.168.1.50:8080/api).| ID | Descrição | Endpoint REST |
|---|---|---|
| RF-V01 | Consultar e exibir todas as tarefas cadastradas ao carregar a página ou acionar o botão de recarga. | GET /api/tarefas |
| RF-V02 | Cadastrar nova tarefa com descrição preenchida e status inicial não concluído. | POST /api/tarefas |
| RF-V03 | Atualizar o estado de conclusão (concluida = true/false) de forma assíncrona. |
PUT /api/tarefas/{id} |
| RF-V04 | Modificar a descrição textual de uma tarefa através de modal interativo. | PUT /api/tarefas/{id} |
| RF-V05 | Deletar permanentemente uma tarefa após confirmação do usuário. | DELETE /api/tarefas/{id} |
| ID | Categoria | Descrição |
|---|---|---|
| RNF-V01 | Reatividade | O estado da UI deve ser gerenciado por reatividade nativa do Vue 3 (ref e Composition API). |
| RNF-V02 | Cross-Platform | O código deve compilar tanto para Web SPA quanto para aplicativo móvel Android através do Quasar CLI e Capacitor. |
| RNF-V03 | Resolução de Rede | O cliente deve detectar em tempo de execução se está rodando em ambiente Capacitor e chavear a base URL para 10.0.2.2:8080. |
| RNF-V04 | Feedback Visual | Todas as ações do usuário (sucesso, erro, carregamento) devem acionar componentes nativos do Quasar (Notify, Dialog, QInnerLoading). |
| RNF-V05 | CORS | As requisições originadas do Vite (:5173), Quasar (:9000) e Capacitor (http://localhost) devem ser aceitas pela API via @CrossOrigin. |
flowchart LR
User(("👤 Usuário Web / Mobile"))
subgraph "Aplicação Vue 3 / Quasar"
UC01["UC01: Listar Tarefas com Ordenação"]
UC02["UC02: Inserir Nova Tarefa"]
UC03["UC03: Alternar Conclusão da Tarefa"]
UC04["UC04: Editar Tarefa em Diálogo Modal"]
UC05["UC05: Excluir Tarefa com Alerta"]
end
User --> UC01
User --> UC02
User --> UC03
User --> UC04
User --> UC05
onMounted), o serviço tarefaService.listarTodas() é invocado.QInnerLoading).tarefas.value é atualizado e o template re-renderiza as linhas.QInput e pressiona Enter.POST /api/tarefas com payload { "descricao": "...", "concluida": false }.201 Created, limpa o campo de entrada, emite notificação positiva via $q.notify() e recarrega a lista.QCheckbox de uma linha.@update:model-value dispara tarefaService.atualizar(id, { ...tarefa, concluida: novoValor }).200 OK.text-strike) e fundo atenuado.| Caso de Uso | História de Usuário | Requisito Funcional | Requisito Não-Funcional | Arquivo Responsável |
|---|---|---|---|---|
| UC01 | US-V01 | RF-V01 | RNF-V01, RNF-V04 | TarefasPage.vue, tarefaService.js |
| UC02 | US-V02 | RF-V02 | RNF-V01, RNF-V04 | TarefasPage.vue, QInput |
| UC03 | US-V03 | RF-V03 | RNF-V01, RNF-V04 | TarefasPage.vue, QCheckbox |
| UC04 | US-V04 | RF-V04 | RNF-V01, RNF-V04 | TarefasPage.vue, QDialog |
| UC05 | US-V05 | RF-V05 | RNF-V01, RNF-V04 | TarefasPage.vue, $q.dialog() |
A Parte 06 organiza-se em dois ambientes complementares compartilhando a mesma API central:
fullstack_tarefas_06_spring_vue/
├── assets/
│ ├── arquitetura_vue_quasar_ecosystem.svg ← Diagrama conceitual Single Codebase
│ └── fluxo_requisicao_quasar_api.svg ← Diagrama do ciclo reativo Quasar/Axios
├── web-vue/ ← Aplicação Web SPA pura (Vue 3 + Vite)
│ ├── src/
│ │ ├── services/tarefaService.js ← Comunicação HTTP com Axios (Porta 8080)
│ │ ├── App.vue ← Componente principal da Web SPA
│ │ └── main.js ← Inicialização do Vue 3
│ ├── package.json ← Dependências do Vite e Vue 3
│ └── vite.config.js ← Configuração do bundler Vite
├── mobile-quasar/ ← Aplicação Cross-Platform com Quasar Framework
│ ├── src/
│ │ ├── boot/axios.js ← Plugin de boot com resolução inteligente de IP
│ │ ├── services/tarefaService.js ← Camada de serviço desacoplada
│ │ ├── layouts/MainLayout.vue ← Layout mobile responsivo (Header e Toolbar)
│ │ ├── pages/TarefasPage.vue ← Tela completa com componentes Quasar
│ │ ├── router/routes.js ← Definição de rotas do Vue Router
│ │ └── css/app.scss ← Paleta de cores corporativa
│ ├── quasar.config.js ← Configuração de compilação Quasar
│ ├── capacitor.config.json ← Configuração do empacotamento móvel
│ └── package.json ← Dependências do Quasar e Capacitor
└── index.md ← Este Guia Padrão Ouro Educacional
web-vue/)No arquivo web-vue/src/services/tarefaService.js, configuramos o Axios apontando para a porta 8080:
import axios from 'axios'
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api'
const apiClient = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
})
export default {
getTarefas() {
return apiClient.get('/tarefas')
},
addTarefa(tarefa) {
return apiClient.post('/tarefas', tarefa)
},
updateTarefa(id, tarefa) {
return apiClient.put(`/tarefas/${id}`, tarefa)
},
deleteTarefa(id) {
return apiClient.delete(`/tarefas/${id}`)
},
}
mobile-quasar/)src/boot/axios.js):No Quasar, criamos um arquivo de boot que injeta a instância do Axios e adapta o IP automaticamente:
import { boot } from 'quasar/wrappers'
import axios from 'axios'
import { Platform } from 'quasar'
const getBaseUrl = () => {
// Se estiver rodando dentro do Capacitor no Android, usa o alias do host:
if (Platform.is.capacitor && Platform.is.android) {
return 'http://10.0.2.2:8080/api'
}
// No navegador web ou desktop:
return process.env.API_URL || 'http://localhost:8080/api'
}
const api = axios.create({
baseURL: getBaseUrl(),
headers: { 'Content-Type': 'application/json' },
})
export default boot(({ app }) => {
app.config.globalProperties.$axios = axios
app.config.globalProperties.$api = api
})
export { api }
src/layouts/MainLayout.vue):Utiliza o sistema de grid e barras de ferramentas do Quasar:
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated class="bg-primary text-white">
<q-toolbar>
<q-avatar icon="check_circle" color="secondary" text-color="white" />
<q-toolbar-title>
<div class="text-weight-bold">Minhas Tarefas</div>
<div class="text-caption text-indigo-2">Quasar Framework · Mobile & Web</div>
</q-toolbar-title>
<q-btn flat round dense icon="refresh" @click="eventoRecarregar" title="Recarregar" />
</q-toolbar>
</q-header>
<q-page-container>
<router-view @recarregar-disparado="registrarHandler" />
</q-page-container>
</q-layout>
</template>
src/pages/TarefasPage.vue):Implementa listagem com QList, caixas de seleção QCheckbox, modal de edição QDialog, feedback sonoro/visual via $q.notify() e diálogo de exclusão via $q.dialog():
<template>
<q-page class="q-pa-md max-container">
<!-- Formulário em Card -->
<q-card flat bordered class="q-mb-md">
<q-card-section class="q-pa-sm">
<q-input
v-model="novaTarefaDescricao"
placeholder="O que precisa ser feito?"
outlined
dense
@keyup.enter="adicionarTarefa"
>
<template v-slot:after>
<q-btn color="primary" icon="add" label="Adicionar" unelevated @click="adicionarTarefa" />
</template>
</q-input>
</q-card-section>
</q-card>
<!-- Lista com QList e QItem -->
<q-card flat bordered>
<q-list separator v-if="tarefas.length > 0">
<q-item v-for="tarefa in tarefas" :key="tarefa.id" clickable v-ripple>
<q-item-section side>
<q-checkbox
:model-value="tarefa.concluida"
@update:model-value="(val) => alternarStatus(tarefa, val)"
color="secondary"
/>
</q-item-section>
<q-item-section>
<q-item-label :class="{ 'text-strike text-grey-6': tarefa.concluida }">
{{ tarefa.descricao }}
</q-item-label>
</q-item-section>
<q-item-section side>
<div class="row q-gutter-xs">
<q-btn flat round dense color="info" icon="edit" @click.stop="abrirModalEdicao(tarefa)" />
<q-btn flat round dense color="negative" icon="delete" @click.stop="confirmarExclusao(tarefa)" />
</div>
</q-item-section>
</q-item>
</q-list>
</q-card>
</q-page>
</template>
A API central deve estar ativa na porta 8080:
# Na pasta da Parte 01:
cd catalogo_projetos/fullstack_tarefas_01_spring_api/api-spring
./mvnw spring-boot:run
# Ou via Docker Compose na Parte 05:
cd ../../fullstack_tarefas_05_arquitetura
docker compose up -d
cd catalogo_projetos/fullstack_tarefas_06_spring_vue/frontend
npm install
npm run dev
Acesse no navegador: http://localhost:5173.
cd catalogo_projetos/fullstack_tarefas_06_spring_vue/mobile-quasar
npm install
npx quasar dev
Acesse no navegador: http://localhost:9000.
Para abrir o projeto diretamente no Android Studio com suporte ao emulador oficial:
npx quasar dev -m capacitor -T android
O Quasar compilará os assets web, sincronizará a pasta dist/spa com o projeto Android nativo do Capacitor e iniciará a aplicação conectando-se automaticamente em http://10.0.2.2:8080.
| # | Ação de Teste | Comportamento Esperado | Resultado |
|---|---|---|---|
| 01 | Abrir a Web SPA em localhost:5173. |
Lista de tarefas é carregada da API na porta 8080; exibe badge verde de conexão. | [x] PASS |
| 02 | Cadastrar tarefa via Web SPA. | Tarefa é salva no backend; aparece imediatamente na listagem. | [x] PASS |
| 03 | Abrir o Quasar em localhost:9000 ou no emulador. |
A mesma tarefa cadastrada na Web SPA é exibida de forma sincronizada no Quasar. | [x] PASS |
| 04 | Marcar tarefa como concluída no Quasar. | Texto ganha efeito tachado; ao atualizar a Web SPA, o status reflete a conclusão. | [x] PASS |
| 05 | Editar descrição via modal QDialog. |
Diálogo abre com texto preenchido; ao salvar, a notificação Notify confirma sucesso. |
[x] PASS |
| 06 | Excluir tarefa confirmando no $q.dialog. |
Item é removido do banco; mensagem de feedback surge no topo da tela. | [x] PASS |
Parabéns! Você dominou a transição da Web para o Mobile com o ecossistema Vue:
10.0.2.2.Na próxima etapa da esteira (Parte 07), você verá a correspondência equivalente no ecossistema React: React 19 ➔ React Native + Expo (“Construa interfaces com componentes e deixe o ecossistema complementar”), renderizando componentes nativos reais com o motor Yoga Flexbox!