Armazenamento local no Ionic: persistência com Storage e preferências

Capítulo 12

Tempo estimado de leitura: 10 minutos

+ Exercício

O que é armazenamento local no Ionic (e por que usar)

Armazenamento local é a capacidade do app persistir dados no dispositivo do usuário para que eles continuem disponíveis mesmo após fechar o aplicativo. Em apps Ionic/Capacitor, isso é útil para: preferências (tema, idioma), sessão (tokens e dados mínimos do usuário), e cache (respostas de API para reduzir chamadas e melhorar performance).

No ecossistema Ionic/Capacitor, duas abordagens comuns são:

  • Capacitor Preferences: chave/valor simples, ideal para preferências e pequenos dados.
  • Ionic Storage (pacote @ionic/storage-angular): uma camada de armazenamento mais flexível, com suporte a diferentes drivers (IndexedDB, SQLite via plugins, etc.), boa para dados maiores/estruturados e cenários de cache mais elaborados.

Quando usar Preferences vs Storage

Caso de usoMelhor opçãoPor quê
Preferências simples (tema, idioma, flags)PreferencesAPI direta, leve, chave/valor
Guardar token de sessão (simples)PreferencesPersistência rápida; cuidado com segurança (ver observação)
Cache de respostas de APIStorageMais flexível, melhor para objetos e estratégias de expiração
Dados maiores/coleções (listas, histórico)StorageMelhor para volumes maiores e drivers mais robustos

Observação importante de segurança: armazenamento local não é criptografado por padrão. Para dados sensíveis (ex.: tokens com alto impacto, dados pessoais), considere soluções com criptografia/secure storage (plugins específicos). Aqui o foco é persistência local comum para preferências e cache.

Persistindo preferências com Capacitor Preferences

Instalação e importação

Em projetos Capacitor, o plugin Preferences costuma estar disponível via @capacitor/preferences. Se necessário, instale:

npm i @capacitor/preferences

Uso no código:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

import { Preferences } from '@capacitor/preferences';

Passo a passo: salvar e ler uma preferência (tema)

Vamos persistir uma preferência theme com valores como light ou dark.

const THEME_KEY = 'theme';

export async function setTheme(theme: 'light' | 'dark') {
  await Preferences.set({
    key: THEME_KEY,
    value: theme,
  });
}

export async function getTheme(): Promise<'light' | 'dark'> {
  const { value } = await Preferences.get({ key: THEME_KEY });
  return (value === 'dark' ? 'dark' : 'light');
}

export async function clearTheme() {
  await Preferences.remove({ key: THEME_KEY });
}

Boas práticas:

  • Centralize as chaves em constantes para evitar erros de digitação.
  • Defina valores padrão quando value vier null.
  • Evite salvar objetos diretamente sem serialização (ver seção de serialização).

Persistência com Ionic Storage (mais flexível)

Instalação

npm i @ionic/storage-angular

Configuração (passo a passo)

Em Angular, você normalmente registra o módulo do Storage e inicializa a instância. Exemplo em um módulo principal (ou módulo de core):

import { IonicStorageModule } from '@ionic/storage-angular';

@NgModule({
  imports: [
    IonicStorageModule.forRoot(),
  ],
})
export class AppModule {}

Crie um serviço para encapsular o acesso e garantir que o storage foi criado antes do uso:

import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';

@Injectable({ providedIn: 'root' })
export class AppStorageService {
  private ready: Promise<void>;

  constructor(private storage: Storage) {
    this.ready = this.storage.create().then(() => undefined);
  }

  private async ensureReady() {
    await this.ready;
  }

  async set<T>(key: string, value: T): Promise<void> {
    await this.ensureReady();
    await this.storage.set(key, value);
  }

  async get<T>(key: string): Promise<T | null> {
    await this.ensureReady();
    const v = await this.storage.get(key);
    return (v ?? null) as T | null;
  }

  async remove(key: string): Promise<void> {
    await this.ensureReady();
    await this.storage.remove(key);
  }

  async clear(): Promise<void> {
    await this.ensureReady();
    await this.storage.clear();
  }
}

Esse padrão evita bugs comuns em que o app tenta usar o Storage antes de ele estar pronto.

Serialização segura de objetos (evitando corrupção e erros)

Nem sempre você vai salvar apenas strings. Preferências e cache frequentemente envolvem objetos. Existem dois caminhos:

  • Preferences: armazena string. Você precisa serializar com JSON.stringify.
  • Ionic Storage: costuma aceitar objetos diretamente, mas ainda é útil padronizar a estrutura e validar ao ler.

Estratégia recomendada: envelope versionado + validação

Ao salvar objetos, use um “envelope” com versão e timestamp. Isso ajuda em migrações e invalidação.

type StoredEnvelope<T> = {
  v: number;       // versão do schema
  savedAt: number; // Date.now()
  data: T;
};

function makeEnvelope<T>(data: T, v = 1): StoredEnvelope<T> {
  return { v, savedAt: Date.now(), data };
}

Exemplo: salvando objeto em Preferences com try/catch

import { Preferences } from '@capacitor/preferences';

const USER_PREFS_KEY = 'user_prefs';

type UserPrefs = {
  language: string;
  notificationsEnabled: boolean;
};

export async function saveUserPrefs(prefs: UserPrefs) {
  const payload = makeEnvelope(prefs, 1);
  await Preferences.set({
    key: USER_PREFS_KEY,
    value: JSON.stringify(payload),
  });
}

export async function loadUserPrefs(): Promise<UserPrefs> {
  const { value } = await Preferences.get({ key: USER_PREFS_KEY });
  if (!value) {
    return { language: 'pt-BR', notificationsEnabled: true };
  }

  try {
    const parsed = JSON.parse(value) as StoredEnvelope<unknown>;

    // validação mínima (evita quebrar o app com dados antigos/corrompidos)
    if (!parsed || typeof parsed !== 'object') throw new Error('invalid');
    if ((parsed as any).v !== 1) throw new Error('unsupported version');

    const data = (parsed as any).data;
    if (!data || typeof data.language !== 'string' || typeof data.notificationsEnabled !== 'boolean') {
      throw new Error('invalid data');
    }

    return data as UserPrefs;
  } catch {
    // fallback seguro
    return { language: 'pt-BR', notificationsEnabled: true };
  }
}

Esse padrão evita que uma mudança de formato ou um valor corrompido cause erro em cascata no app.

Cache simples de API: reduzindo chamadas e melhorando performance

Cache local é armazenar o resultado de uma chamada de API por um tempo (TTL) e reutilizar esse resultado enquanto estiver válido. Isso melhora performance, reduz consumo de rede e deixa o app mais responsivo.

Conceitos essenciais

  • Chave de cache: deve identificar unicamente a requisição (endpoint + parâmetros).
  • TTL (time-to-live): tempo máximo que o cache é considerado válido.
  • Invalidação: remover/ignorar cache quando expira ou quando dados mudam.
  • Atualização: buscar dados novos e substituir o cache.

Modelo de item de cache

type CacheEntry<T> = {
  v: number;
  savedAt: number;
  ttlMs: number;
  data: T;
};

function isExpired(entry: CacheEntry<unknown>): boolean {
  return Date.now() > entry.savedAt + entry.ttlMs;
}

Passo a passo: serviço de cache com Ionic Storage

Exemplo de um cache genérico que salva e lê entradas com TTL. Ele também oferece um método getOrFetch para encapsular a lógica.

import { Injectable } from '@angular/core';
import { AppStorageService } from './app-storage.service';

@Injectable({ providedIn: 'root' })
export class CacheService {
  private prefix = 'cache:';

  constructor(private appStorage: AppStorageService) {}

  private key(k: string) {
    return this.prefix + k;
  }

  async set<T>(key: string, data: T, ttlMs: number, v = 1): Promise<void> {
    const entry = { v, savedAt: Date.now(), ttlMs, data };
    await this.appStorage.set(this.key(key), entry);
  }

  async get<T>(key: string): Promise<T | null> {
    const entry = await this.appStorage.get<any>(this.key(key));
    if (!entry) return null;

    // validação mínima
    if (typeof entry.savedAt !== 'number' || typeof entry.ttlMs !== 'number') return null;
    if (Date.now() > entry.savedAt + entry.ttlMs) return null;

    return entry.data as T;
  }

  async invalidate(key: string): Promise<void> {
    await this.appStorage.remove(this.key(key));
  }

  async getOrFetch<T>(
    key: string,
    ttlMs: number,
    fetcher: () => Promise<T>
  ): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null) return cached;

    const fresh = await fetcher();
    await this.set(key, fresh, ttlMs);
    return fresh;
  }
}

Como definir uma chave de cache correta

Uma chave de cache precisa incluir o que muda o resultado. Exemplo: lista de produtos com paginação e filtro.

function productsCacheKey(page: number, category?: string) {
  const c = category ?? 'all';
  return `products?page=${page}&category=${encodeURIComponent(c)}`;
}

Exemplo prático: cache de lista com TTL e atualização manual

Suponha um serviço que busca produtos. Você pode usar getOrFetch com TTL de 5 minutos:

const TTL_5_MIN = 5 * 60 * 1000;

async function loadProducts(page: number, category?: string) {
  const key = productsCacheKey(page, category);

  return this.cache.getOrFetch(key, TTL_5_MIN, async () => {
    // aqui você chamaria seu serviço HTTP real
    return await this.api.getProducts(page, category);
  });
}

Para forçar atualização (por exemplo, pull-to-refresh), invalide e busque novamente:

async function refreshProducts(page: number, category?: string) {
  const key = productsCacheKey(page, category);
  await this.cache.invalidate(key);
  return await this.cache.getOrFetch(key, 5 * 60 * 1000, () => this.api.getProducts(page, category));
}

Estratégia “stale-while-revalidate” (rápida e com dados atualizados)

Uma estratégia simples e muito efetiva é: mostrar cache imediatamente (mesmo que esteja perto de expirar) e atualizar em segundo plano. Isso melhora a percepção de velocidade.

Uma forma prática de implementar é retornar o cache se existir e disparar uma atualização assíncrona que substitui o cache. Em UI, você pode refletir a atualização quando o dado novo chegar.

async function loadWithBackgroundRefresh<T>(
  key: string,
  ttlMs: number,
  fetcher: () => Promise<T>,
  onUpdate: (fresh: T) => void
) {
  const cached = await this.cache.get<T>(key);
  if (cached !== null) {
    // dispara atualização sem bloquear
    fetcher()
      .then(async fresh => {
        await this.cache.set(key, fresh, ttlMs);
        onUpdate(fresh);
      })
      .catch(() => {
        // ignore falhas de atualização em background
      });

    return cached;
  }

  const fresh = await fetcher();
  await this.cache.set(key, fresh, ttlMs);
  return fresh;
}

Quando usar:

  • Telas de listagem onde “um pouco desatualizado” é aceitável por alguns segundos.
  • Apps que precisam ser rápidos em redes instáveis.

Invalidação de cache: quando e como

Além do TTL, você deve invalidar cache quando ocorrerem eventos que mudam os dados:

  • Após criar/editar/remover um item: invalide a lista relacionada.
  • Troca de usuário (logout/login): limpe caches associados ao usuário.
  • Mudança de filtros globais (ex.: idioma/região): invalide chaves dependentes.

Padrão de chaves com escopo de usuário

Para evitar vazar cache entre usuários, inclua um identificador (ex.: userId) na chave.

function userScopedKey(userId: string, key: string) {
  return `u:${userId}:${key}`;
}

Limpeza seletiva por prefixo (opcional)

Se você precisar remover vários itens de cache, uma abordagem é manter uma lista de chaves salvas (índice) ou usar um prefixo e armazenar esse índice. Exemplo simples com índice:

const CACHE_INDEX_KEY = 'cache:index';

async function trackCacheKey(appStorage: AppStorageService, key: string) {
  const index = (await appStorage.get<string[]>(CACHE_INDEX_KEY)) ?? [];
  if (!index.includes(key)) {
    index.push(key);
    await appStorage.set(CACHE_INDEX_KEY, index);
  }
}

async function clearAllCache(appStorage: AppStorageService) {
  const index = (await appStorage.get<string[]>(CACHE_INDEX_KEY)) ?? [];
  for (const k of index) {
    await appStorage.remove(k);
  }
  await appStorage.set(CACHE_INDEX_KEY, []);
}

Ao salvar cache, você chamaria trackCacheKey com a chave completa (incluindo prefixo). Isso permite limpar tudo sem apagar preferências.

Checklist prático (decisão rápida)

  • Precisa salvar preferências pequenas? Use Preferences.
  • Precisa salvar objetos/coleções e implementar cache com TTL? Use Ionic Storage.
  • Vai salvar objetos? Use envelope versionado e validação ao ler.
  • Cache de API: defina chave, TTL e regras de invalidação após mutações.

Agora responda o exercício sobre o conteúdo:

Em um app Ionic/Capacitor, qual combinação de escolhas é mais adequada para salvar a preferência de tema e implementar cache de respostas de API com expiração (TTL)?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Preferences é indicado para preferências simples em formato chave/valor. Para cache de API com TTL e dados mais estruturados, Ionic Storage é mais flexível, permitindo armazenar objetos e controlar expiração/invalidação.

Próximo capitúlo

Capacitor no Ionic: configuração do projeto e ciclo de vida no dispositivo

Arrow Right Icon
Capa do Ebook gratuito Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript
57%

Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.