Visão Geral do Projeto
Este artigo detalha a criação de uma solução mobile para casais organizarem e compartilharem momentos gastronômicos. O aplicativo resolve problemas cotidianos como decisões de cardápio, registro de pratos favoritos e documentação de experiências culinárias compartilhadas, utilizando o framework ArkUI do HarmonyOS.
Requisitos Funcionais
O sistema implementa dois perfis distintos com funcionalidades específicas:
- Cozinheiro: Gerencia receitas, processa pedidos e controla o conteúdo
- Comensal: Explora cardápio, realiza pedidos e interage com fotos
| Módulo | Funcionalidades do Cozinheiro | Funcionalidades do Comensal |
|---|---|---|
| Gestão de Papéis | Assuma o papel de cozinheiro | Assuma o papel de comansal |
| Receitas | CRIAR, EDITAR, EXCLUIR pratos | VISUALIZAR e buscar receitas |
| Pedidos | ACEITAR e finalizar solicitações | FAZER pedidos e acompanhar status |
| Galeria | UPLOAD de imagens | Visualizar e curtir fotos |
| Configurações | Alterar perfil e limpar dados | Alterar perfil e limpar dados |
Stack Tecnológico
- Framework: ArkUI (Declarativo)
- Linguagem: ArkTS
- Persistência: Banco de Dados Relacional (RDB)
- Gerenciamento de Estado: Decorators @State, @Prop, @Link
- Armazenamento de Configurações: Preferences API
Estrutura de Diretórios
entry/src/main/ets/
├── utilitarios/ # Classes utilitárias
│ ├── Constantes.ets # Definições globais
│ ├── Logger.ets # Sistema de logs
│ ├── BancoDadosUtil.ets # Gerenciamento RDB
│ └── ConfigUtil.ets # Armazenamento de preferências
├── modelos/ # Entidades de dados
│ ├── Receita.ets # Modelo de receita
│ ├── Pedido.ets # Modelo de pedido
│ ├── FotoGaleria.ets # Modelo de imagem
│ └── ConfiguracaoCasal.ets # Configuração do espaço
├── paginas/ # Componentes de UI
│ ├── TelaInicial.ets # Splash screen
│ ├── TelaSelecaoPapel.ets # Escolha de perfil
│ ├── TelaPrincipalChef.ets # Dashboard do cozinheiro
│ ├── TelaPrincipalComensal.ets # Dashboard do comensal
│ ├── GerenciadorReceitas.ets # CRUD de receitas
│ ├── GerenciadorPedidos.ets # Controle de pedidos
│ ├── GaleriaFotos.ets # Album de imagens
│ └── Ajustes.ets # Configurações
└── entryability/
└── EntryAbility.ets # Ponto de entrada
Modelagem do Banco de Dados
Tabela de Configuração do Espaço
CREATE TABLE IF NOT EXISTS configuracao_casal (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome_espaco TEXT NOT NULL,
nome_cozinheiro TEXT NOT NULL,
nome_comensal TEXT NOT NULL,
data_criacao INTEGER NOT NULL
)
Tabela de Receitas
CREATE TABLE IF NOT EXISTS receitas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
categoria INTEGER NOT NULL,
imagem_url TEXT,
ingredientes TEXT,
modo_preparo TEXT,
data_criacao INTEGER NOT NULL
)
Tabela de Pedidos
CREATE TABLE IF NOT EXISTS pedidos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
receita_id INTEGER NOT NULL,
receita_titulo TEXT NOT NULL,
receita_imagem TEXT,
status INTEGER DEFAULT 0,
observacao TEXT,
data_pedido INTEGER NOT NULL,
data_aceitacao INTEGER,
data_conclusao INTEGER,
FOREIGN KEY (receita_id) REFERENCES receitas(id) ON DELETE CASCADE
)
Tabela de Fotos
CREATE TABLE IF NOT EXISTS fotos_galeria (
id INTEGER PRIMARY KEY AUTOINCREMENT,
caminho_imagem TEXT NOT NULL,
descricao TEXT,
data_registro INTEGER NOT NULL
)
Implementação do Sistema de Papéis
Tela de Boas-Vindas
A tela inicial gerencia a inicialização do aplicativo e redirecionamento baseado no perfil armazenado.
// TelaInicial.ets
@Entry
@Component
struct TelaInicial {
private TAG: string = 'TelaInicial'
async onPageShow(): Promise<void> {
try {
await this.configurarAmbiente()
await this.verificarPerfilExistente()
} catch (erro) {
Logger.erro(this.TAG, `Falha na inicialização: ${erro}`)
this.navegarParaSelecao()
}
}
private async configurarAmbiente(): Promise<void> {
await BancoDadosUtil.inicializar(this.getContext())
await ConfigUtil.inicializar(this.getContext())
}
private async verificarPerfilExistente(): Promise<void> {
const perfilAtual = await ConfigUtil.obterValor('perfil_ativo', '')
switch(perfilAtual) {
case 'cozinheiro':
this.navegarParaPrincipal('paginas/TelaPrincipalChef')
break
case 'comensal':
this.navegarParaPrincipal('paginas/TelaPrincipalComensal')
break
default:
this.navegarParaSelecao()
}
}
private navegarParaPrincipal(rota: string): void {
router.replaceUrl({ url: rota })
}
private navegarParaSelecao(): void {
router.replaceUrl({ url: 'paginas/TelaSelecaoPapel' })
}
build() {
Column({ space: 12 }) {
Progress({ value: 30, type: ProgressType.Circular })
.width(60)
Text('Inicializando...')
.fontSize(16)
.fontColor('#666')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
</void></void></void>
Interface de Seleção de Perfil
Permite configurar o espaço compartilhado e definir papéis para cada parceiro.
// TelaSelecaoPapel.ets
@Entry
@Component
struct TelaSelecaoPapel {
@State nomeEspaco: string = ''
@State nomeCozinheiro: string = ''
@State nomeComensal: string = ''
@State papelSelecionado: string = ''
async aboutToAppear() {
await this.carregarConfiguracaoExistente()
}
private async carregarConfiguracaoExistente(): Promise<void> {
const predicados = new relationalStore.RdbPredicates('configuracao_casal')
const resultado = await BancoDadosUtil.consultar(predicados)
if (resultado.goToFirstRow()) {
this.nomeEspaco = resultado.getString(resultado.getColumnIndex('nome_espaco'))
this.nomeCozinheiro = resultado.getString(resultado.getColumnIndex('nome_cozinheiro'))
this.nomeComensal = resultado.getString(resultado.getColumnIndex('nome_comensal'))
}
resultado.close()
}
private async salvarPerfil(papel: string): Promise<void> {
if (!this.validarFormulario()) {
promptAction.showToast({ message: 'Preencha todos os campos' })
return
}
const dados: relationalStore.ValuesBucket = {
nome_espaco: this.nomeEspaco.trim(),
nome_cozinheiro: this.nomeCozinheiro.trim(),
nome_comensal: this.nomeComensal.trim(),
data_criacao: Date.now()
}
const predicados = new relationalStore.RdbPredicates('configuracao_casal')
const registros = await BancoDadosUtil.contarRegistros(predicados)
try {
if (registros > 0) {
await BancoDadosUtil.atualizar('configuracao_casal', dados, predicados)
} else {
await BancoDadosUtil.inserir('configuracao_casal', dados)
}
await ConfigUtil.salvarValor('perfil_ativo', papel)
const destino = (papel === 'cozinheiro')
? 'paginas/TelaPrincipalChef'
: 'paginas/TelaPrincipalComensal'
router.replaceUrl({ url: destino })
} catch (erro) {
Logger.erro('TelaSelecaoPapel', `Erro ao salvar: ${erro}`)
}
}
private validarFormulario(): boolean {
return this.nomeEspaco.length > 0 &&
this.nomeCozinheiro.length > 0 &&
this.nomeComensal.length > 0
}
build() {
Column({ space: 20 }) {
Text('Configure seu Espaço')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 30 })
TextInput({ placeholder: 'Nome do espaço' })
.onChange((valor) => this.nomeEspaco = valor)
.maxLength(50)
TextInput({ placeholder: 'Apelido do cozinheiro' })
.onChange((valor) => this.nomeCozinheiro = valor)
.maxLength(30)
TextInput({ placeholder: 'Apelido do comensal' })
.onChange((valor) => this.nomeComensal = valor)
.maxLength(30)
Row({ space: 16 }) {
Button('Cozinhar')
.width('40%')
.onClick(() => this.salvarPerfil('cozinheiro'))
Button('Comer')
.width('40%')
.onClick(() => this.salvarPerfil('comensal'))
}
.margin({ top: 20 })
}
.width('90%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 10, color: '#1A000000' })
}
}
</void></void>
Camada de Persistência
Implementação do utilitário de preferências para manter o estado do perfil ativo.
// ConfigUtil.ets
export class ConfigUtil {
private static instancia: preferences.Preferences | null = null
static async inicializar(contexto: Context): Promise<void> {
if (!ConfigUtil.instancia) {
ConfigUtil.instancia = await preferences.getPreferences(contexto, 'config_app_casal')
}
}
static async salvarValor(chave: string, valor: string): Promise<boolean> {
try {
await ConfigUtil.instancia?.put(chave, valor)
await ConfigUtil.instancia?.flush()
return true
} catch (erro) {
Logger.erro('ConfigUtil', `Falha ao salvar ${chave}: ${erro}`)
return false
}
}
static async obterValor(chave: string, padrao: string): Promise<string> {
try {
return await ConfigUtil.instancia?.get(chave, padrao) || padrao
} catch (erro) {
Logger.erro('ConfigUtil', `Falha ao ler ${chave}: ${erro}`)
return padrao
}
}
}
</string></boolean></void>
Desenvolvimento das Telas Principais
Dashboard do Cozinheiro
// TelaPrincipalChef.ets
@Entry
@Component
struct TelaPrincipalChef {
@State configuracao: ConfiguracaoCasal = {
id: 0,
nome_espaco: 'Meu Espaço',
nome_cozinheiro: 'Chef',
nome_comensal: 'Parceiro',
data_criacao: 0
}
async onPageShow(): Promise<void> {
await this.carregarDadosCasal()
}
private async carregarDadosCasal(): Promise<void> {
const predicados = new relationalStore.RdbPredicates('configuracao_casal')
const resultado = await BancoDadosUtil.consultar(predicados)
if (resultado.goToFirstRow()) {
this.configuracao = {
id: resultado.getLong(resultado.getColumnIndex('id')),
nome_espaco: resultado.getString(resultado.getColumnIndex('nome_espaco')),
nome_cozinheiro: resultado.getString(resultado.getColumnIndex('nome_cozinheiro')),
nome_comensal: resultado.getString(resultado.getColumnIndex('nome_comensal')),
data_criacao: resultado.getLong(resultado.getColumnIndex('data_criacao'))
}
}
resultado.close()
}
build() {
Column() {
this.cabecalhoPersonalizado()
Grid() {
GridItem() { this.cardFuncao('Receitas', $r('app.media.ic_cookbook')) }
.onClick(() => router.pushUrl({ url: 'paginas/GerenciadorReceitas' }))
GridItem() { this.cardFuncao('Pedidos', $r('app.media.ic_orders')) }
.onClick(() => router.pushUrl({ url: 'paginas/GerenciadorPedidos' }))
GridItem() { this.cardFuncao('Galeria', $r('app.media.ic_gallery')) }
.onClick(() => router.pushUrl({ url: 'paginas/GaleriaFotos' }))
GridItem() { this.cardFuncao('Ajustes', $r('app.media.ic_settings')) }
.onClick(() => router.pushUrl({ url: 'paginas/Ajustes' }))
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.height('60%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
cabecalhoPersonalizado() {
Row() {
Text(`Chef: ${this.configuracao.nome_cozinheiro}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
Text(this.configuracao.nome_espaco)
.fontSize(14)
.fontColor('#666')
}
.width('100%')
.padding({ left: 20, right: 20, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
}
@Builder
cardFuncao(titulo: string, icone: Resource) {
Column({ space: 8 }) {
Image(icone)
.width(48)
.height(48)
Text(titulo)
.fontSize(14)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
}
</void></void>
Dashboard do Comensal
Similar ao dashboard do cozinheiro, mas com ações limitadas a visualização e criação de pedidos.
Problemas de Implementação e Soluções
Problema: Exibição de Valores Padrão Após Configuração
Sintoma: Após salvar as configurações, a tela principal exibe "Chef" ao invés do nome configurado.
Causa: Incompatibilidade entre o modelo de dados TypeScript e o schema SQLite, com falha silenciosa na inserção.
Solução:
// BancoDadosUtil.ets
static async inicializarComRecuperacao(contexto: Context): Promise<void> {
const config: relationalStore.StoreConfig = {
name: 'casal_culinaria.db',
securityLevel: relationalStore.SecurityLevel.S1
}
try {
BancoDadosUtil.banco = await relationalStore.getRdbStore(contexto, config)
await BancoDadosUtil.criarTabelas()
} catch (erro) {
Logger.erro('BancoDadosUtil', `Erro crítico: ${erro}`)
// Força recriação em caso de corrupção
await relationalStore.deleteRdbStore(contexto, 'casal_culinaria.db')
BancoDadosUtil.banco = await relationalStore.getRdbStore(contexto, config)
await BancoDadosUtil.criarTabelas()
}
}
private static async criarTabelas(): Promise<void> {
const sqlConfiguracao = `
CREATE TABLE IF NOT EXISTS configuracao_casal (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome_espaco TEXT NOT NULL,
nome_cozinheiro TEXT NOT NULL,
nome_comensal TEXT NOT NULL,
data_criacao INTEGER NOT NULL
)
`
await BancoDadosUtil.banco.executeSql(sqlConfiguracao)
}
</void></void>
Problema: Perda de Estado Após Limpeza de Cache
Sintoma: Limpar os dados do aplicativo remove as preferências, mas mantém o banco, causando estado inconsistente.
Solução: Implementar sincronização bidirecional na inicialização:
// TelaSelecaoPapel.ets
async sincronizarEstado(): Promise<void> {
const perfilSalvo = await ConfigUtil.obterValor('perfil_ativo', '')
const configuracaoExiste = await this.verificarConfiguracaoBanco()
if (!perfilSalvo && configuracaoExiste) {
// Recupera automaticamente baseado nos dados do banco
await ConfigUtil.salvarValor('perfil_ativo', 'cozinheiro') // Valor padrão
promptAction.showDialog({
title: 'Recuperação Detectada',
message: 'Encontramos dados do seu espaço. Selecione seu papel novamente.',
buttons: [{ text: 'OK', color: '#FF6B6B' }]
})
}
}
</void>
Considerações Finais
Esta implementação estabelece a base para um aplicativo colaborativo com separação clara de responsabilidades entre perfis. A arquitetura modular permite evolução para funcionalidades como sincronização em nuvem e compartilhamento de receitas entre múltiplos casais.
Os próximos dseenvolvimentos abordarão o CRUD completo de receitas, workflow de pedidos e integração de galeria de fotos com armazenamento de mídia otimizado.