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 uso | Melhor opção | Por quê |
|---|---|---|
| Preferências simples (tema, idioma, flags) | Preferences | API direta, leve, chave/valor |
| Guardar token de sessão (simples) | Preferences | Persistência rápida; cuidado com segurança (ver observação) |
| Cache de respostas de API | Storage | Mais flexível, melhor para objetos e estratégias de expiração |
| Dados maiores/coleções (listas, histórico) | Storage | Melhor 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/preferencesUso no código:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
valueviernull. - 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-angularConfiguraçã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.