Dia 15 — Fechamento do ano

Informatica · Conteudo · publicado em 05/10/2026
Dia 15 de 15

Fechamento do ano

Aula 1

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:

PassoComandoO que volta
criarINSERTinsertId e rowsAffected
lerSELECTrows, array de linhas
alterarUPDATErowsAffected
apagarDELETErowsAffected

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
Aula 2

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