Faz alguns meses que mantenho sozinho um app de acompanhamento de séries para iOS e Android. Este post não é para vender o app. É o registro dos problemas que me custaram mais tempo, com o código que ficou no lugar. Talvez poupe alguém de repetir.

Aquisição de Aplicativo de Séries: Experiência e Desafios

TanStack Query, Expo e Firebase

Fizemos uso de TanStack Query 5 para dados remotos, persistidos com @tanstack/query-async-storage-persister + AsyncStorage. O Firebase 12 (Auth + Firestore) foi nosso backend, mais Cloud Functions para o que precisava rodar no servidor (lembretes, webhook de assinatura, convites). Expo 47 + React Native 0.86, com expo-router para navegação. Um código só para iOS, Android (e web, que uso pouco). Expo foi a escolha por ser a única opção para manter dois apps nativos.

Problemas Encenados

  1. O Cache Persistido que Derrubou o Android

Persistir o cache do React Query em disco parecia ganho fácil: as telas abrem já preenchidas, sem "Carregando". Configurei gcTime de 7 dias e persisti tudo. O problema: o persister grava o cache inteiro numa única chave do AsyncStorage, e esse valor atravessa a ponte JS→nativo de uma vez. Cada busca digitada virava uma entrada nova (['search', texto]) guardada por 7 dias, junto com feed, grupos, comentários e likes. Em produção o blob chegou a ~47 MB e o ReadableNativeArray.importArray estourou o heap no Android. Crash fatal, não exceção tratável.

  1. staleTime: Nem "Sempre Buscar", Nem "Confiar no Cache de 7 Dias"

O detalhe de série (['show', id]) é usado por quem percorre a biblioteca inteira: o agendador de notificações e o widget. Testei os dois extremos: staleTime: 0 → em cada abertura e volta do background, uma busca por série, todas ao mesmo tempo. Em aparelho de entrada isso virou tempestade de alocação. O GC segurando o mutator lock aparecia como ANR na thread principal. Cache persistido de 7 dias sem teto → notificação baseada em data antiga do TMDB, ou seja, aviso de episódio que foi adiado.

  1. Notificação de Episódio Novo: Fuso, Next vs Last e Cancelamento que Apagava Tudo

Os avisos são locais (expo-notifications, trigger por data), marcados para as 18h do dia de exibição e só dentro de uma janela de 10 dias. Três bugs reais:

a) Fuso de Origem: A air_date do TMDB é do país de origem. Série americana que estreia à noite lá gerava aviso um dia antes de dar pra assistir aqui. Agora existe um localReleaseDate(air_date, origin_country, networks) que devolve o dia local em que o episódio certamente já saiu.

b) O Episódio que deixa de ser "Próximo": O TMDB move o episódio de next_episode_to_air para last_episode_to_air no instante em que ele vai ao ar na origem. Como o reagendamento cancela os avisos anteriores, o usuário pode receber notificações de episódios que já foram exibidos.

Conclusão

Aprendemos que persistência é opt-in por tipo de query, nunca o cache inteiro. Colocamos teto de tamanho e planejamos a migração de quem já está com o estado ruim. Também aprendemos que staleTime precisa ser maior que o maxAge do persister para garantir que as queries não sejam descartadas antes de serem restauradas. E, por fim, a notificação de episódio novo exigiu atenção especial para lidar com fuso de origem e cancelamento de notificações.

Aqui está o código relevante:

```javascript
// Só o que vale disco: detalhe de série/filme e coleção.
const PERSISTED_QUERY_PREFIXES = ['show', 'movie', 'collection'];

export function shouldPersistQuery(query) {
// Só sucesso: ao passar um shouldDehydrateQuery próprio, o default do TanStack deixa de valer
if (query.state.status!== 'success') return false;
const [prefix] = query.queryKey;
return typeof prefix === 'string' && PERSISTED_QUERY_PREFIXES.includes(prefix);
}

// Teto de ~2 MB: acima disso é melhor ficar sem cache em disco do que crashar
export const cappedStorage = {
getItem: (k) => AsyncStorage.getItem(k),
removeItem: (k) => AsyncStorage.removeItem(k),
setItem: async (k, v) => {
if (v.length > 2 1024 1024) return AsyncStorage.removeItem(k);
return AsyncStorage.setItem(k, v);
},
};

// Notificação de episódio novo: fuso, next vs last e cancelamento que apagava tudo
const localReleaseDate = (air_date, origin_country, networks) => {
// Implementação do fuso de origem
};

export const SHOW_DETAILS_STALE_TIME = 6 60 60; // 6 horas

export const shouldPersistQuery = shouldPersistQuery;
export const cappedStorage = cappedStorage;
export const localReleaseDate = localReleaseDate;
export const SHOW_DETAILS_STALE_TIME = SHOW_DETAILS_STALE_TIME;
```

Esses aprendizados ajudarão a garantir uma experiência mais consistente e eficiente para os usuários do aplicativo de séries.

Tags: Aquisição de Aplicativo, Expo, Firebase, TanStack Query, Notificações Locais, Fuso de Origem, Série de TV, Firebase Auth, Firestore, Cloud Functions, Expo Router, React Native, Expo Notifications, Expo Localization, React Native Google Mobile Ads, React Native Purchases, Expo Widgets, TanStack Query Async Storage Persister, AsyncStorage, GCTime, StaleTime, LocalReleaseDate, Fim de Série, Next Episode, Last Episode, Air Date, Origin Country, Networks, Exibição, Exibição Anterior, Exibição Futura, Exibição Atual, Exibição Próxima, Exibição Antiga, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exibição Anterior, Exibição Próxima, Exibição Atual, Exibição Futura, Exib

Aplicativo Aquisicao Desafios Experiencia Series