Dia 1 — Por que o dado precisa sobreviver ao app fechar

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

Por que o dado precisa sobreviver ao app fechar

Aula 1

O estado em memória e o que ele não resolve

O estado em memória e o que ele não resolve

useState guarda o dado em uma variável do JavaScript, e essa variável vive na memória do processo enquanto o app está aberto. É memória, não disco: quando o app fecha, o sistema mata o processo e o valor vai junto. Nada em useState diz "grave isto"; ele devolve o valor e uma função que troca o valor na próxima renderização. Toda a lista de tarefas do aluno fica em memória até o fim, e isso é a escolha certa para muita coisa — só não para tudo.

O sintoma do limite é o mais chato de um aplicativo: o usuário cria dez itens, passa o dia mexendo neles, fecha o aplicativo e abre de novo — o fechar o app é o caso mais curto do problema. A lista está vazia, sem aviso e sem erro no console. O bug não aparece em lugar nenhum e o diagnóstico quase sempre sai errado — "o useState não salvou". Salvou: guardou em memória, que é exatamente o que foi pedido a ele. O nome do problema é dado volátil, e a pergunta útil não é por que o estado sumiu, e sim onde este estado deveria estar.

É o que perder dado significa na prática: não é o aplicativo quebrar, e sim o aplicativo funcionar perfeitamente e abrir vazio amanhã. Esse detalhe é o que separa o bug de persistência de todo o resto.

O que a memória resolve e o que não resolve

Estado em memória é a resposta certa para dado recriável: se o usuário apagar a lista inteira e o app puxar de novo, a tela já tem o que precisa. Também é a resposta certa para dado derivado, aquele que sai de outro dado, porque recalcular é barato.

const [itens, setItens] = useState([]);
const [filtro, setFiltro] = useState('todas');

const total = itens.reduce((soma, item) => soma + (item.feita ? 0 : item.valor), 0);
const visiveis = filtro === 'todas' ? itens : itens.filter((item) => item.grupo === filtro);

O critério que separa os dois casos não é o tamanho nem a quantidade de itens: é se perder o dado é aceitável. Perder o filtro ativo é um incômodo de meio segundo; perder o que o usuário criou à mão é trabalho dele jogado fora, e o aplicativo que faz isso perde a confiança do usuário na primeira semana. A regra de bolso é uma pergunta sobre a origem: se a tela pode repuxar esse dado de algum lugar, ele pode ficar em memória; se ele nasceu do dedo do usuário e ninguém mais tem cópia, ele precisa sobreviver ao fechamento.

O lugar onde essa distinção aparece na prática é o botão de marcar como feita. A linha da tela tem feita em true porque o estado mudou, e no fim do dia esse true só existia naquela variável. Um usuário que marcou dez tarefas, fechou o aplicativo e abriu de novo para contar o que falta descobre que fez trabalho dobrado.

DadoVale guardar em memória
Texto do campo que está sendo digitadoSim, enquanto a tela estiver aberta
Filtro e ordem em uso na listaSim
Lista que vem do servidor e pode voltarSim
Lista que o usuário criou e quer que sobrevivaNão

A tabela se lê como uma pergunta de produto, não como regra de sintaxe. O mesmo campo muda de coluna conforme quem o produziu: o texto digitado num formulário é estado de tela enquanto a pessoa digita, e vira dado persistido no instante em que o INSERT confirma.

A fronteira entre memória e disco

Persistência local é o nome do conjunto de técnicas que põem o dado fora da memória do processo — em arquivo, em armazenamento do sistema, em banco — para que ele sobreviver ao fechamento. A fronteira cabe em uma frase: memória morre com o processo, disco não. E o processo morre mais vezes do que o aluno imagina: fechar o aplicativo é o caso óbvio, mas reiniciar o celular, o sistema liberar memória por falta de bateria e a atualização do próprio sistema encerram o processo do mesmo jeito. Qualquer um dos três já apaga o que estava só na variável.

O useState continua existindo depois que a persistência entra, e passa a ter outro papel — a cópia em tela do que o banco tem. Essa troca de papel é a mudança conceitual do trimestre: antes o estado era a fonte da verdade, e depois ele passa a ser uma cópia que precisa ser recarregada.

O padrão que o aplicativo inteiro segue a partir daí é: salvar no banco, reler do banco, colocar no estado. Não é "gravar e manter em memória", porque estado que só existe na tela e banco que só existe depois de recarregar são duas verdades que divergem. Reler custa dois milissegundos e elimina a classe de bug em que a tela mostra uma coisa e o banco tem outra.

O exemplo desta página mostra o ciclo inteiro em quatro linhas de impressão: a variável recebe três tarefas, a tela monta um View com um Text por tarefa pendente, e então a variável é criada de novo, vazia. Isso é reabrir o app: mesmo código, mesmo arquivo, lista zerada. A partir daqui, o nome armazenamento local designa o lugar em que o dado vai para que essa última linha imprima três tarefas em vez de zero.

Exemplo

// Estado em memória: o dado vive na variável enquanto o processo existe.
// O exemplo grava uma lista, mostra a tela, "fecha o app" e reabre.
const { View, Text } = require('react-native');

let tarefas = [];

function adicionar(titulo) {
  tarefas.push({ id: tarefas.length + 1, titulo: titulo, feita: false });
}

function pendentes() {
  return tarefas.filter((tarefa) => !tarefa.feita);
}

adicionar('Revisar o WHERE');
adicionar('Ler o capitulo 4');
adicionar('Enviar o relatorio');

console.log('com o app aberto:', tarefas.length, 'tarefas');

// a arvore que a tela monta: um `View` por lista e um `Text` por tarefa
const lista = (
  <View>
    {pendentes().map((tarefa) => (
      <Text key={tarefa.id}>{tarefa.titulo}</Text>
    ))}
  </View>
);
console.log('a tela desenhou', lista.props.children.length, 'linhas');

// fechar o app e o fim do processo: a variavel nasce de novo, vazia
tarefas = [];

console.log('depois de reabrir:', tarefas.length, 'tarefas');
console.log('o que a tela mostraria agora:', tarefas);

Saída real

com o app aberto: 3 tarefas
a tela desenhou 3 linhas
depois de reabrir: 0 tarefas
o que a tela mostraria agora: []
Aula 2

As três opções de onde guardar o dado

As três opções de onde guardar o dado

Três lugares resolvem a pergunta "onde o dado fica": a memória do processo, o armazenamento chave-valor do sistema e o banco relacional embarcado. Nenhum é o melhor em absoluto. A escolha vem do tipo de dado e, principalmente, da quantidade de perguntas que o aplicativo precisa fazer sobre ele.

Os dois primeiros locais têm nome de biblioteca: o chave-valor do React Native é o AsyncStorage, e o banco embarcado é o SQLite. O resto da página usa os dois nomes juntos — AsyncStorage para preferência e SQLite para coleção — porque é assim que o aplicativo real decide.

O armazenamento chave-valor é o mais simples do trio. setItem grava uma chave com uma string, getItem devolve a string, removeItem apaga a chave, clear apaga tudo. Quando o valor é objeto ou array, ele passa por JSON.stringify na entrada e JSON.parse na saída. O que ele não faz é filtrar, ordenar nem contar: se a tela quer só as tarefas concluídas, o aplicativo lê a lista inteira e filtra em JavaScript, todo acesso.

Os dois primeiros são local: o dado fica em um lugar dentro do aparelho, gerenciado pelo sistema, e só aquele aplicativo enxerga. O terceiro é o mesmo tipo de almacenamiento com estrutura de consulta por cima. A alternativa a todo trio seria guardar no remoto, num servidor acessado por rede, que traz a vantagem do dado compartilhado entre aparelhos e o custo de depender de conexão — e de um backend, que é assunto do ano seguinte.

OpçãoO que guardaComo consultaRelaciona dadosVolume
Memóriaobjeto e arrayem JavaScriptsim, na mãoenquanto o app estiver aberto
Chave-valoruma string por chavesó depois de ler tudonãopoucos registros
Banco embarcadotabela com colunasSELECT com WHEREchave estrangeira e JOINmilhares de linhas

O exemplo desta página põe as três para responder as mesmas perguntas e imprime o custo de cada uma: o par chave e valor devolve quatro linhas inteiras para filtrar o que eram três pendentes, e o banco devolve três linhas depois de examinar quatro. O número final é o que interessa — é a diferença entre uma comparar opções no papel e uma quantificação do custo real.

O que cada uma resolve

A memória resolve estado de tela: o texto do campo, a posição da rolagem, o filtro ativo, se a lista pode ser recriada. O chave-valor resolve preferência: tema escuro, última ordenação escolhida, identificador do usuário. O banco resolve coleção: tudo que o usuário cria, edita e apaga, e que precisa continuar lá depois do fechamento.

A segunda diferença que decide é quando o aplicativo precisa contar, somar ou relacionar. Um contador de pendências guardado em chave-valor significa JSON.parse da lista inteira a cada leitura; no banco a mesma pergunta é SELECT COUNT(*) FROM tarefas WHERE feita = 0, respondida sem trazer uma linha sequer para o JavaScript.

O volume separa os dois primeiros por uma linha muito concreta: chave-valor serve para dado pequeno — tema, último id, uma preferência. Ele foi feito para guardar pouquíssimas chaves, e cada gravação reescreve a string inteira. Collection grande não é o problema dele.

O terceiro critério é dado relacionado: quando uma tarefa aponta para uma categoria e o filtro é por nome de categoria, a pergunta já atravessa duas coleções. Chave-valor não tem como responder isso sem trazer as duas listas inteiras para a memória e unir em JavaScript — o que funciona, e fica mais lento a cada linha adicionada.

A escolha se faz pela pergunta

O SQL aparece no momento em que a pergunta muda de forma. Enquanto a pergunta é "qual é o valor da chave tema?", chave-valor é mais curto e mais rápido. Quando a pergunta passa a ser "quais tarefas estão vencidas e pertencem à categoria trabalho?", ela ganha estrutura — três entidades, uma condição composta, uma ordem — e o banco responde isso sem devolver o resto.

É por isso que a decisão se toma pela pergunta que o aplicativo faz ao dado, e não por preferência do time. Guardar uma lista de tarefas em chave-valor funciona até a primeira pergunta composta; a partir daí, cada resposta custa ler, converter e filtrar a lista inteira.

A única decisão ruim aqui é a do meio: começar com banco para tudo. Ela traz migration, PRAGMA e abrir e migrar o arquivo antes da primeira tela aparecer, para guardar uma preferência que cabia em duas linhas. A ordem prática é a do trimestre: armazenamento local simples para preferência, banco para coleção.

A resposta de uma linha para a dúvida de sempre — "eu escolho o banco" ou "eu escolho chave-valor" — é: escreva a pergunta que a tela faz ao dado. Se a resposta precisa atravessar duas coleções, ou contar, ou filtrar por condição composta, é banco. Se é o valor de uma chave, é escolher armazenamento no simples.

Exemplo

// As tres opcoes de armazenamento respondendo as mesmas tres
// perguntas: guardar, consultar por condicao e relacionar. O exemplo conta
// quantos registros cada opcao precisou tocar para responder.
const tarefas = [
  { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0, categoria_id: 1 },
  { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1, categoria_id: 1 },
  { id: 3, titulo: 'Enviar o relatorio', prazo: '2026-09-08', feita: 0, categoria_id: 2 },
  { id: 4, titulo: 'Comprar cafe', prazo: '2026-09-07', feita: 0, categoria_id: 2 },
];
const categorias = [
  { id: 1, nome: 'estudo' },
  { id: 2, nome: 'trabalho' },
];

// 1. memoria: o objeto existe enquanto o processo existir
const memoria = { tarefas: tarefas.slice() };
console.log('memoria  -> guarda', Object.keys(memoria).length, 'colecao');

// 2. chave-valor: uma string por chave. Toda consulta passa por JSON.parse
const chaveValor = new Map();
chaveValor.set('tarefas', JSON.stringify(tarefas));

function consultarChaveValor(chave) {
  return JSON.parse(chaveValor.get(chave));
}
console.log('chave-valor -> devolve', consultarChaveValor('tarefas').length, 'linhas inteiras');

// 3. banco: uma tabela por nome, e a consulta escolhe o que volta
const banco = { tarefas: tarefas.slice(), categorias: categorias.slice() };

function consultarTabela(tabela, condicao) {
  let examinadas = 0;
  const linhas = [];
  for (const linha of banco[tabela]) {
    examinadas += 1;
    if (condicao(linha)) linhas.push(linha);
  }
  return { linhas: linhas, examinadas: examinadas };
}

const pendentes = consultarTabela('tarefas', (linha) => linha.feita === 0);
console.log('banco     -> devolve', pendentes.linhas.length, 'linhas, examinou', pendentes.examinadas);

// relacionar e o que so o banco faz sem trazer as duas tabelas inteiras
const trabalho = consultarTabela('tarefas', (linha) => linha.categoria_id === 2).linhas;
console.log('tarefas da categoria trabalho:', trabalho.map((t) => t.titulo));

Saída real

memoria  -> guarda 1 colecao
chave-valor -> devolve 4 linhas inteiras
banco     -> devolve 3 linhas, examinou 4
tarefas da categoria trabalho: [ 'Enviar o relatorio', 'Comprar cafe' ]