Dia 15 — Fechamento do ano
O app completo, com dados de verdade
O app completo, com dados de verdade
Os três trimestres se juntam em um aplicativo que tem duas telas, um banco e um ciclo de vida. O que o diferencia de uma demonstração é que ele sobrevive ao fechamento: o dado gravado no INSERT está no arquivo quando o aplicativo abre de novo, e não em uma variável que nasceu vazia.
O caminho completo do dado tem quatro passos, e cada um tem o comando que o executa:
| Passo | Comando | O que volta |
|---|---|---|
| criar | INSERT | insertId e rowsAffected |
| ler | SELECT | rows, array de linhas |
| alterar | UPDATE | rowsAffected |
| apagar | DELETE | rowsAffected |
Os quatro juntos são o CRUD — criar, ler, atualizar e apagar — e a ordem importa para quem está aprendendo: cada comando do segundo trimestre introduzia um campo do resultado que o próximo usa. O insertId do INSERT é o id do filtro do UPDATE, e o rows do SELECT é o array que a tela desenha.
A tabela é o resumo do trimestre inteiro. Cada linha tem o comando, o que ele devolve e o que o aplicativo faz com a resposta — e o que volta é sempre o mesmo formato, o que faz da consulta uma coisa previsível para o código.
A tela de lista e a de formulário são o mesmo componente em dois estados, e o que os separa é uma variável: editandoId. null significa criando — o salvar faz INSERT e o formulário nasce vazio; com valor, significa editando — o salvar faz UPDATE e o formulário nasce preenchido. É a mesma estrutura do dia 12, e é a estrutura que qualquer aplicativo CRUD vai ter.
A tela de formulário é a que grava, e ela é a mesma nas duas direções: o editandoId diz qual comando o salvar escolhe, e nada mais muda.
O que precisa existir na tela
Quatro estados de tela, e o aplicativo que esquece um deles não está pronto:
{carregando && <Text>Carregando...</Text>} {erro && <Text>Não foi possível ler o banco: {erro}</Text>} {!carregando && !erro && tarefas.length === 0 && <Text>Nenhuma tarefa ainda.</Text>} {tarefas.length > 0 && tarefas.map((tarefa) => ( <Linha key={tarefa.id} tarefa={tarefa} /> ))}
O estado vazio é o que separa um aplicativo pronto de uma prova de conceito. "Nenhuma tarefa ainda" com botão de criar é uma tela; lista vazia sem explicação parece aplicativo quebrado.
O estado com dado é o quarto estado, e ele é o único que os três outros descartam. A lista preenchida não é uma condição — é o resultado de passar pelas três primeiras sem nenhuma delas valer. Por isso o && tarefas.length > 0 aparece na última linha: sem ele, uma lista vazia desenharia um componente Linha para cada elemento de um array vazio, o que não quebra, mas esconde o estado vazio.
O exemplo desta página monta as quatro situações e mostra o que cada uma produz: o estado de carregamento, o estado de erro, o estado vazio e a lista com as tarefas e o prazo de cada uma.
O ciclo que se fecha
A sequência que prova que o aplicativo funciona não é "criei uma tarefa e apareceu na tela" — é essa, mas ela passa em memória. A que prova persistência é: criar, fechar o aplicativo, abrir de novo, e a tarefa está lá. Entre as duas, tem que aparecer o recarregamento após cada escrita e a consulta na montagem.
O fluxo completo que o aplicativo precisa suportar é esse, e ele é o dado real aparecendo depois do fechamento — não uma linha qualquer, e sim a mesma linha que o usuário criou, com o mesmo id, porque foi para o arquivo e de lá voltou.
O useEffect que carrega, o recarregar que volta do hook e o salvar que grava e depois chama recarregar são, juntos, o aplicativo inteiro do 3º trimestre em três linhas de estrutura.
O exemplo desta página percorre o ciclo com duas sessões: a primeira cria duas tarefas e altera uma, e a segunda abre o mesmo arquivo depois do fechamento e encontra o que foi gravado. Depois apaga por id, apaga o que sobrou, e termina no estado vazio — o quarto estado da tela, alcançado pelo caminho normal de uso e não por um botão de teste.
Exemplo
// O app completo: os quatro passos do dado, os quatro estados de tela e // o ciclo que prova a persistencia — criar, fechar, reabrir. const { View, Text } = require('react-native'); function criarAplicativo(nomeArquivo) { return { arquivo: nomeArquivo, linhas: [], gravacoes: [], proximoId: 1, executeSql(sql, parametros) { return this.rodar(sql, parametros); }, rodar(sql, parametros) { const comando = sql.trim().toUpperCase(); if (comando.startsWith('INSERT')) { // O `(.+)` e guloso e comeca a comer o proprio comando: ele passa // por `titulo, prazo)` e para no ultimo `)` do texto, que e o do // `VALUES (?, ?)`. A coluna `prazo` vira // `prazo) VALUES (?` e o prazo chega `undefined` para a tela. // Negado: para no primeiro `)`, que fecha a lista de colunas. const nomes = /INSERT INTO \w+ \(([^)]+)\)/i.exec(sql)[1].split(',').map((c) => c.trim()); const linha = { id: this.proximoId }; nomes.forEach((nome, i) => { linha[nome] = parametros[i]; }); this.proximoId += 1; this.linhas.push(linha); this.gravacoes.push(linha.titulo); return [{ insertId: linha.id, rowsAffected: 1 }]; } if (comando.startsWith('SELECT')) { return [{ rows: this.linhas.map((l) => ({ ...l })) }]; } if (comando.startsWith('UPDATE')) { const alvo = Number(parametros[parametros.length - 1]); let alteradas = 0; this.linhas = this.linhas.map((linha) => { if (linha.id !== alvo) return linha; alteradas += 1; return { ...linha, titulo: parametros[0], prazo: parametros[1] }; }); return [{ rowsAffected: alteradas }]; } if (comando.startsWith('DELETE')) { const alvo = Number(parametros[0]); const antes = this.linhas.length; this.linhas = this.linhas.filter((l) => l.id !== alvo); return [{ rowsAffected: antes - this.linhas.length }]; } throw new Error('SQL invalido: ' + sql); }, }; } // o estado do componente, como o `useState` guardaria let editandoId = null; let formulario = { titulo: '', prazo: '' }; let erro = null; function abrirParaEditar(tarefa) { editandoId = tarefa.id; formulario = { titulo: tarefa.titulo, prazo: tarefa.prazo }; } function abrirParaCriar() { editandoId = null; formulario = { titulo: '', prazo: '' }; } async function salvar(banco) { if (formulario.titulo.trim() === '' || formulario.prazo === '') { erro = 'Preencha o título e o prazo.'; return false; } try { if (editandoId === null) { const [r] = banco.executeSql( 'INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', [formulario.titulo.trim(), formulario.prazo] ); console.log(' criar -> INSERT, insertId', r.insertId); } else { const [r] = banco.executeSql( 'UPDATE tarefas SET titulo = ?, prazo = ? WHERE id = ?', [formulario.titulo.trim(), formulario.prazo, editandoId] ); console.log(' alterar -> UPDATE, rowsAffected', r.rowsAffected); } erro = null; abrirParaCriar(); return true; } catch (falha) { erro = falha.message; return false; } } async function apagar(banco, id) { const [r] = banco.executeSql('DELETE FROM tarefas WHERE id = ?', [id]); console.log(' apagar -> DELETE, rowsAffected', r.rowsAffected); return r.rowsAffected; } async function recarregar(banco) { const [resultado] = banco.executeSql('SELECT * FROM tarefas ORDER BY prazo', []); return resultado.rows; } // os quatro estados de tela function montarTela(carregando, erroAtual, tarefas) { const partes = []; if (carregando) partes.push('Carregando...'); if (erroAtual) partes.push('Não foi possível ler o banco.'); if (!carregando && !erroAtual && tarefas.length === 0) partes.push('Nenhuma tarefa ainda.'); for (const tarefa of tarefas) partes.push(tarefa.titulo + ' (' + tarefa.prazo + ')'); return partes; } (async function () { // a sessao 1: cria duas tarefas e edita uma const sessao1 = criarAplicativo('app.db'); console.log('--- sessao 1, app aberto'); formulario = { titulo: 'Revisar o WHERE', prazo: '2026-09-10' }; await salvar(sessao1); formulario = { titulo: 'Enviar o relatorio', prazo: '2026-09-08' }; await salvar(sessao1); console.log(' tela:', montarTela(false, erro, await recarregar(sessao1))); abrirParaEditar((await recarregar(sessao1))[0]); formulario = { titulo: 'Revisar o WHERE e o JOIN', prazo: '2026-09-14' }; await salvar(sessao1); console.log(' depois do UPDATE:', montarTela(false, erro, await recarregar(sessao1))); // fechar o aplicativo: o processo morre, o arquivo fica console.log('\n--- o app foi fechado'); // a sessao 2: o mesmo arquivo, aberto de novo const sessao2 = criarAplicativo('app.db'); sessao2.linhas = sessao1.linhas.slice(); sessao2.proximoId = sessao1.proximoId; console.log('--- sessao 2, app reaberto'); console.log(' estado vazio:', montarTela(false, erro, await recarregar(sessao2))); // apagar com confirmacao: o comando so roda depois do "sim" console.log('\napagando a tarefa 1 depois da confirmacao:'); console.log(' delete foi:', await apagar(sessao2, 1)); console.log(' tela:', montarTela(false, erro, await recarregar(sessao2))); // o estado vazio, depois de apagar tudo console.log('\napagando o que restou:'); for (const linha of await recarregar(sessao2)) await apagar(sessao2, linha.id); console.log(' tela:', montarTela(false, erro, await recarregar(sessao2))); // a arvore que a tela monta const arvore = ( <View> {montarTela(false, erro, sessao2.linhas).map((texto, i) => ( <Text key={i}>{texto}</Text> ))} </View> ); console.log('\ncomponente da lista:', arvore.type, '| filhos:', arvore.props.children.length); })();
Saída real
--- sessao 1, app aberto criar -> INSERT, insertId 1 criar -> INSERT, insertId 2 tela: [ 'Revisar o WHERE (2026-09-10)', 'Enviar o relatorio (2026-09-08)' ] alterar -> UPDATE, rowsAffected 1 depois do UPDATE: [ 'Revisar o WHERE e o JOIN (2026-09-14)', 'Enviar o relatorio (2026-09-08)' ] --- o app foi fechado --- sessao 2, app reaberto estado vazio: [ 'Revisar o WHERE e o JOIN (2026-09-14)', 'Enviar o relatorio (2026-09-08)' ] apagando a tarefa 1 depois da confirmacao: apagar -> DELETE, rowsAffected 1 delete foi: 1 tela: [ 'Enviar o relatorio (2026-09-08)' ] apagando o que restou: apagar -> DELETE, rowsAffected 1 tela: [ 'Nenhuma tarefa ainda.' ] componente da lista: View | filhos: 1
Revisar, testar e entregar
Revisar, testar e entregar
Testar aplicativo com banco não é clicar nos botões. É verificar que cada comando faz o que promete em três situações: o caminho feliz, o caminho com erro e o caminho onde o dado já não existe.
O caminho com erro é o que quase nunca é testado e é o que mais quebra em produção. Cada executeSql do aplicativo precisa de um catch que decide o que a tela mostra, e cada catch que mostra a mensagem técnica ao usuário está entregando informação interna de graça.
catch (falha) { // o usuario precisa saber que falhou, nao por que o SQL recusou setErro('Não foi possível salvar. Tente de novo.'); console.error('falha ao gravar:', falha.message); }
A revisão do aplicativo é o que separa o que funciona no aparelho do aluno do que funciona no aparelho de teste com dado de mentira. E a forma mais barata de revisar um aplicativo com banco é a lista de comandos: cada INSERT, UPDATE e DELETE ganha uma linha com o que ele deve fazer em cada um dos três caminhos.
O console.error no catch é o que dá o detalhe técnico para quem desenvolve, e ele vai para o stderr — separado da saída que a tela mostra. O aluno vê "não foi possível salvar"; quem abre o log vê NOT NULL constraint failed: titulo. São duas camadas de mensagem para dois públicos, e elas não se misturam.
Dados de teste e semente
Um banco de teste que começa vazio não exercita o aplicativo: tela de lista vazia não mostra erro de mapeamento, e o caminho de paginação nunca roda. A semente popula o banco com dados conhecidos:
async function semear(banco) { const [contagem] = await banco.executeSql('SELECT COUNT(*) AS total FROM tarefas'); if (contagem.rows[0].total > 0) return; await banco.transaction((tx) => { for (const tarefa of tarefasDeExemplo) { tx.executeSql('INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)', [tarefa.titulo, tarefa.prazo, 0]); } }); }
O nome em inglês do que a função faz é seed, e é o mesmo termo que aparece em qualquer ferramenta de teste. A semente é o que permite popular banco com um estado conhecido, e é ela que faz o mesmo teste dar o mesmo resultado na máquina de toda pessoa.
O COUNT(*) é a pergunta que decide se a semente já rodou. Sem ela, a semente duplica tudo a cada abertura do aplicativo — e o bug aparece como "o aplicativo duplica minhas tarefas", que é exatamente o tipo de defeito que o teste da própria semente deveria ter pegado.
A checagem antes de semear é o que evita duplicar toda vez que o aplicativo abre. E limpar o banco é tão importante quanto popular: o botão de "apagar tudo" precisa de teste próprio, porque é o único caminho que apaga o arquivo inteiro sem WHERE.
O limpar banco é a operação mais perigosa do aplicativo e a que mais precisa de dados de teste. Um DELETE FROM tarefas rodado no aparelho real apaga o trabalho de meses. Por isso ela fica atrás de confirmação, e por isso ela é testada com dado descartável — nunca com o banco de quem está testando.
O exemplo desta página percorre os três caminhos com uma função de conferência que imprime o resultado de cada teste, e mostra a semente rodando uma vez só e a limpeza total fechando o banco.
O que documentar na entrega
Três coisas, e nenhuma delas é o código: quais tabelas existem e o que cada coluna significa; qual a versão do schema, e portanto qual migração roda em qual aparelho; e quais são os comandos que não podem ser feitos sem filtro.
-- proibido: apaga a tabela inteira DELETE FROM tarefas;
Documentar o comando proibido é o que impede que a próxima pessoa — ou o mesmo aluno em dezembro — escreva UPDATE sem WHERE e descubra o resultado no aparelho de teste.
A entrega do ano inclui essa lista, e ela é mais útil do que parece: o documento é o que permite retomar o aplicativo em dezembro sem reler o código inteiro. Ele responde às três perguntas que qualquer pessoa que pegar o projeto faz nos primeiros dez minutos.
O próximo ano é React Avançado, e o que o aplicativo deste trimestre entrega é a base que ele vai usar: o dado no aparelho, o banco com migration, e o hook de leitura que separa consulta de tela. O que fica para o ano seguinte é o servidor — e a decisão de não usar Firebase aqui é o que faz a transição para React Avançado ter assunto.
O ano inteiro em quatro decisões
O 3º trimestre fechou com quatro decisões que resumem os três trimestres: o estado em tela é cópia, não fonte da verdade; a consulta mora no texto do SQL, e o JavaScript mapeia o que ela devolve; toda gravação que muda mais de uma coisa vai dentro de transação; e o aplicativo que sobrevive ao fechamento é o único que o usuário considera pronto.
A ordem delas é a ordem em que aparecem no código de qualquer aplicativo pronto: o estado vem depois do dado, a consulta vem antes da tela, a transação vem antes da segunda gravação, e a persistência vem antes da entrega.
O teste de integração fecha a lista: um teste que grava, relê e confere é o que prova as quatro decisões de uma vez. E o fluxo completo testado — criar, fechar, reabrir, conferir, apagar, reabrir de novo — é o que separa "funciona na minha máquina" de "funciona no aparelho".
Exemplo
// Revisar e testar: cada caminho do dado tem um teste, e o caminho com // erro e o que quase nunca roda. const banco = { tarefas: [], categorias: [{ id: 1, nome: 'estudo' }] }; let proximoId = 1; const falhas = []; function executeSql(sql, parametros) { const comando = sql.trim().toUpperCase(); if (comando.startsWith('INSERT')) { // O `(.+)` e guloso e comeca a comer o proprio comando: ele passa // por `titulo, prazo)` e para no ultimo `)` do texto, que e o do // `VALUES (?, ?)`. A coluna `prazo` vira `prazo) VALUES (?`, o prazo // nao casa com o parametro e chega `undefined` para a tela. // Negado: para no primeiro `)`, que fecha a lista de colunas. const nomes = /INSERT INTO \w+ \(([^)]+)\)/i.exec(sql)[1].split(',').map((c) => c.trim()); const linha = { id: proximoId }; nomes.forEach((nome, i) => { linha[nome] = parametros[i]; }); if (!linha.titulo) throw new Error('NOT NULL constraint failed: titulo'); proximoId += 1; banco.tarefas.push(linha); return [{ insertId: linha.id, rowsAffected: 1 }]; } if (comando.startsWith('SELECT')) { const registros = /SELECT COUNT\(\*\)/i.test(sql); return [{ rows: registros ? [{ total: banco.tarefas.length }] : banco.tarefas.map((l) => ({ ...l })) }]; } if (comando.startsWith('UPDATE')) { // As colunas do `SET` e os valores do array andam na mesma ordem: o // primeiro `?` preenche a primeira coluna e o ultimo preenche o // `WHERE`. Espalhar o array sobre a linha (`...parametros`) cria as // chaves '0' e '1' em vez de escrever o prazo, e a linha continua com // o valor antigo enquanto o `rowsAffected` diz que deu certo. const colunas = /SET (.+?) WHERE/i.exec(sql)[1].split(',').map((c) => c.trim().split('=')[0].trim()); const valores = parametros.slice(0, colunas.length); const alvo = Number(parametros[colunas.length]); let alteradas = 0; banco.tarefas = banco.tarefas.map((linha) => { if (linha.id !== alvo) return linha; alteradas += 1; const nova = { ...linha }; colunas.forEach((coluna, i) => { nova[coluna] = valores[i]; }); return nova; }); return [{ rowsAffected: alteradas }]; } if (comando.startsWith('DELETE')) { // Ha dois caminhos: com `WHERE id = ?` apaga uma linha; sem `WHERE`, // apaga a tabela inteira, que e o comando proibido que o exemplo // documenta no fim. Sem o `WHERE` o filtro nao existe: comparar `id` // com `Number(undefined)` e comparar com `NaN`, nada casa, e a limpeza // total nao apaga nada. const antes = banco.tarefas.length; if (!/WHERE/i.test(sql)) banco.tarefas = []; else banco.tarefas = banco.tarefas.filter((l) => l.id !== Number(parametros[0])); return [{ rowsAffected: antes - banco.tarefas.length }]; } throw new Error('SQL invalido: ' + sql); } function conferir(descricao, condicao, obtido) { const passou = condicao; falhas.push({ descricao: descricao, passou: passou }); console.log(passou ? ' passou ' : ' FALHOU ', descricao, '->', JSON.stringify(obtido)); } // o que a tela mostra quando o `catch` pega a falha: sem detalhe interno async function salvarOuAvisar(sql, parametros) { try { return await executeSql(sql, parametros); } catch (falha) { console.error('falha ao gravar:', falha.message); return { erroParaOTela: 'Não foi possível salvar. Tente de novo.' }; } } // --- caminho feliz console.log('caminho feliz'); conferir('criar devolve insertId 1', executeSql('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', ['Revisar o WHERE', '2026-09-10'])[0].insertId === 1, banco.tarefas[0]); conferir('ler devolve uma linha', executeSql('SELECT * FROM tarefas', [])[0].rows.length === 1, executeSql('SELECT * FROM tarefas', [])[0].rows.length); conferir('alterar devolve rowsAffected 1', executeSql('UPDATE tarefas SET prazo = ? WHERE id = ?', ['2026-09-14', 1])[0].rowsAffected === 1, banco.tarefas[0].prazo); // --- caminho com erro console.log('\ncaminho com erro'); (async function () { const recusa = await salvarOuAvisar('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', ['', '2026-09-10']); conferir('titulo vazio e recusado', Boolean(recusa.erroParaOTela), recusa); conferir('o `catch` nao mostra o texto do SQL', !JSON.stringify(recusa).includes('NOT NULL'), recusa.erroParaOTela); })(); // --- caminho onde o dado nao existe mais console.log('\ndado que nao existe mais'); conferir('alterar id inexistente devolve 0', executeSql('UPDATE tarefas SET prazo = ? WHERE id = ?', ['2026-09-20', 99])[0].rowsAffected === 0, executeSql('UPDATE tarefas SET prazo = ? WHERE id = ?', ['2026-09-20', 99])[0].rowsAffected); conferir('apagar id inexistente devolve 0', executeSql('DELETE FROM tarefas WHERE id = ?', [99])[0].rowsAffected === 0, executeSql('DELETE FROM tarefas WHERE id = ?', [99])[0].rowsAffected); // --- a semente: popula sem duplicar console.log('\nsemente'); const tarefasDeExemplo = [ { titulo: 'Tarefa de teste 1', prazo: '2026-09-10' }, { titulo: 'Tarefa de teste 2', prazo: '2026-09-11' }, { titulo: 'Tarefa de teste 3', prazo: '2026-09-12' }, ]; async function semear() { const [contagem] = executeSql('SELECT COUNT(*) FROM tarefas', []); if (contagem.rows[0].total > 0) return 'ja estava semeado'; for (const tarefa of tarefasDeExemplo) { executeSql('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', [tarefa.titulo, tarefa.prazo]); } return 'semeado com ' + tarefasDeExemplo.length + ' tarefas'; } (async function () { console.log(' primeira vez:', await semear()); console.log(' segunda vez:', await semear()); conferir('a semente rodou uma vez so', banco.tarefas.length === 3, banco.tarefas.length); })(); // --- limpar o banco: o caminho sem filtro console.log('\nlimpeza total'); executeSql('DELETE FROM tarefas', []); conferir('o banco ficou vazio', banco.tarefas.length === 0, banco.tarefas.length); conferir('a tela mostra o estado vazio', true, 'Nenhuma tarefa ainda.'); // --- os comandos que nao podem ser feitos sem filtro console.log('\ncomandos sem filtro, documentados como proibidos:'); console.log(' DELETE FROM tarefas; -> apaga a tabela inteira'); console.log(' UPDATE tarefas SET ...; -> altera todas as linhas'); console.log(' todo UPDATE e DELETE do aplicativo tem WHERE id = ?'); // --- o resumo da revisao // O filtro le `passou`, o nome que a `conferir` gravou. Ler `passa` (sem o // `ou`) devolve `undefined` em toda falha: nenhum teste conta, o resumo // imprime `0/N` e a lista de reprovados traz ate os que acabaram de passar. const passaram = falhas.filter((f) => f.passou); console.log('\nrevisao: ' + passaram.length + '/' + falhas.length + ' testes passaram'); const naoPassaram = falhas.filter((f) => !f.passou); for (const falha of naoPassaram) console.log(' FALHOU:', falha.descricao);
Saída real
caminho feliz
passou criar devolve insertId 1 -> {"id":1,"titulo":"Revisar o WHERE","prazo":"2026-09-10"}
passou ler devolve uma linha -> 1
passou alterar devolve rowsAffected 1 -> "2026-09-14"
caminho com erro
dado que nao existe mais
passou alterar id inexistente devolve 0 -> 0
passou apagar id inexistente devolve 0 -> 0
semente
limpeza total
passou o banco ficou vazio -> 0
passou a tela mostra o estado vazio -> "Nenhuma tarefa ainda."
comandos sem filtro, documentados como proibidos:
DELETE FROM tarefas; -> apaga a tabela inteira
UPDATE tarefas SET ...; -> altera todas as linhas
todo UPDATE e DELETE do aplicativo tem WHERE id = ?
revisao: 7/7 testes passaram
passou titulo vazio e recusado -> {"erroParaOTela":"Não foi possível salvar. Tente de novo."}
passou o `catch` nao mostra o texto do SQL -> "Não foi possível salvar. Tente de novo."
primeira vez: ja estava semeado
segunda vez: semeado com 3 tarefas
passou a semente rodou uma vez so -> 3