App Android de rastreamento de hidratação diária com mascote animado, conquistas, lembretes inteligentes e insights de consumo.
- Visão geral
- Stack
- Funcionalidades
- Arquitetura do projeto
- Como rodar em desenvolvimento
- Como gerar a APK
- Boas práticas do projeto
O Copinho é um app Android construído com React Native + Expo que ajuda o usuário a beber água ao longo do dia. O diferencial é o mascote animado — uma garrafa com rosto que enche à medida que o usuário registra goles — acompanhado de efeitos sonoros, conquistas, lembretes por notificação e um histórico semanal com insights calculados do consumo real.
O projeto nasceu de um design handoff completo (HTML/CSS/JSX de referência) e foi implementado inteiramente em TypeScript com foco em fidelidade visual e experiência mobile nativa.
| Camada | Tecnologia |
|---|---|
| Framework | React Native 0.81.5 + Expo SDK 54 (New Architecture) |
| Linguagem | TypeScript |
| Estado global | Zustand 5 + persist middleware + AsyncStorage |
| Animações | Reanimated 4.3 |
| Gráficos/SVG | react-native-svg 15 |
| Ícones | lucide-react-native 1.14 |
| Notificações | expo-notifications |
| Persistência | @react-native-async-storage/async-storage 1.23 |
| Saúde (Android) | react-native-health-connect |
| Áudio | expo-av + WAV gerado em memória (PCM puro, sem arquivos) |
| Tema | ThemeContext customizado (light/dark via useColorScheme) |
| Build | Gradle 8 · Java 17 · arm64-v8a · minSdk 26 |
- Selecione o volume (50 ml–1.5 L) via slider ou atalhos rápidos
- 5 tipos de líquido com multiplicador de hidratação real (água pura, chá, café, suco, água de coco)
- Histórico do dia com opção de excluir registros individuais
- Célula de celebração animada ao bater a meta diária (confetes + fanfarra sonora)
- Garrafa SVG animada que enche progressivamente conforme o consumo
- 3 formatos: Clássico, Galão, Copinho
- 8 paletas de cor: Aqua, Ocean, Grape, Rose, Coral, Tangerine, Lime, Forest
- Rosto expressivo com olhos piscando e bolhas de água animadas
- Personalização via tela dedicada (cor + formato com preview ao vivo)
- Meta configurável de 1,5 L a 6 L (padrão: 4 L)
- Progresso salvo por dia; reseta automaticamente à meia-noite
- GoalSheet com slider, atalhos populares e texto contextual
- 6 lembretes-template pré-configurados (Ao acordar, Meio da manhã, Almoço, Meio da tarde, Fim de expediente, Jantar)
- Editar horário de qualquer lembrete via TimeEditSheet com spinner de hora/minuto e 6 presets rápidos
- Sugestões de mensagem de notificação por template
- Criar novos lembretes por intervalo (ex.: a cada 2h das 8h às 20h, com preview dos horários gerados) ou horário único (com nome e mensagem personalizados)
- Templates não podem ser deletados; lembretes criados pelo usuário podem
- Tudo reagendado via
expo-notificationsapós qualquer mudança
- 19 badges divididos em 3 categorias:
- Diárias (7): Primeiro gole, Meta cumprida, Passarinho, Só água, 5L boss, Coruja, Ritmo certo
- Semanais (6): Tri-dia, Semana hidratada, Maratona líquida, Fim de semana firme, Madrugadora, Sem cafué
- Mensais (6): 20 em 30, Mês perfeito, Tanque cheio, Novo recorde, Constante, Em evolução
- Hero card por aba (coral / plum / teal-900) com ícone animado e track de progresso
- Badges desbloqueados automaticamente ao atingir critérios
- Gráfico de barras dos últimos 7 dias com linha de meta
- 3 seções de insights calculados em tempo real:
- Padrões da semana — horário de pico, intervalo médio entre goles, gole médio, participação do café
- Comparativos — variação vs. semana anterior, sequência atual vs. recorde
- Recomendações — CTA para criar lembrete às 14h30 direto da tela de histórico
- Tema claro/escuro automático via
useColorScheme() - Todas as cores definidas em
ThemeContext(light/dark); nenhum valor hardcoded nos componentes
- Widget nativo (Kotlin
AppWidgetProvider) que exibe consumo atual / meta - Atualizado via
ReactMethoda cada gole registrado
- Integração opcional com Health Connect (permissão solicitada no primeiro gole)
- Registra cada drink como
HYDRATIONrecord
app/
├── App.tsx # Raiz: ThemeProvider, navegação por estado, overlays
├── src/
│ ├── screens/
│ │ ├── HomeScreen.tsx # Tela principal com Copinho e log do dia
│ │ ├── HistoryScreen.tsx # Histórico semanal + insights
│ │ ├── BadgesScreen.tsx # Conquistas com 3 tabs
│ │ ├── RemindersScreen.tsx # Lembretes com edição/criação
│ │ ├── PersonalizeScreen.tsx # Seletor de cor e formato
│ │ └── OnboardingScreen.tsx # 3 passos de onboarding
│ ├── components/
│ │ ├── Bottle.tsx # Mascote SVG animado
│ │ ├── BottomNav.tsx # Nav bar com FAB central
│ │ ├── AddSheet.tsx # Bottom sheet de registro
│ │ ├── GoalSheet.tsx # Bottom sheet de meta
│ │ ├── TimeEditSheet.tsx # Bottom sheet de edição de lembrete
│ │ └── NewReminderSheet.tsx # Bottom sheet de criação de lembrete
│ ├── store/
│ │ └── useAppStore.ts # Zustand store com persist + AsyncStorage
│ ├── theme/
│ │ ├── ThemeContext.tsx # ThemeProvider + makeStyles helper
│ │ ├── tokens.ts # Colors, FontSizes, Spacing, Radii
│ │ └── colorShades.ts # Paletas das 8 cores da garrafa (oklch → hex)
│ ├── hooks/
│ │ └── useBoolean.ts # Hook utilitário para toggle booleano
│ ├── native/
│ │ └── CopinhoWidgetModule.ts # Bridge JS → Kotlin para atualizar widget
│ └── utils/
│ ├── sounds.ts # Síntese PCM de sons (drop + fanfarra)
│ ├── notifications.ts # Agendamento de lembretes diários
│ └── healthConnect.ts # Integração Health Connect
└── android/
└── app/src/main/java/com/anonymous/copinho/
├── CopinhoWidget.kt # AppWidgetProvider nativo
├── WidgetModule.kt # ReactMethod updateWidget()
└── WidgetPackage.kt # Registro do módulo nativo
O estado da aplicação vive inteiro em um único store Zustand (useAppStore). Toda mutação passa por actions tipadas; componentes apenas leem e chamam actions — nunca escrevem estado diretamente.
addDrink(ml, type)
→ atualiza log, streak, badges, goalMetToday
→ side-effects: playDrop(), writeHydrationRecord(), updateWidget()
→ se cruzou a meta: playFanfare() + showCelebration
O store é persistido via AsyncStorage com partialize (exclui funções e estado transiente como showCelebration). Ao reidratar, o onRehydrateStorage filtra o log para apenas entradas de hoje e migra campos novos (ex.: template nos lembretes).
A navegação é baseada em estado React simples (useState<Screen>), sem React Navigation ou expo-router. Cada tela é renderizada condicionalmente no AppShell. Isso evita conflitos de versão com o Expo SDK e mantém o bundle pequeno.
Nenhum componente usa StyleSheet.create diretamente. Todos usam o helper makeStyles:
const useStyles = makeStyles(c => ({
card: { backgroundColor: c.paper, borderRadius: Radii.card },
title: { color: c.ink, fontSize: FontSizes.h2 },
}));
// No componente:
const styles = useStyles(); // memoizado por temaIsso garante que o dark mode funcione automaticamente em todos os componentes sem lógica extra.
- Node.js 18+
- Android Studio com SDK Android (API 26+)
- Emulador Android ou dispositivo físico
git clone https://github.com/eric-gomides/copinho.git
cd copinho
npm installnpx expo start --androidO app usa New Architecture (Fabric + JSI). Se o emulador travar, reinicie via
adb reboot.
A pasta android/ está versionada com o código nativo do widget. Se rodar expo prebuild --clean, ela será sobrescrita e os arquivos do widget serão perdidos. Nesse caso, restaure os arquivos via git checkout -- android/ antes de compilar.
O build exige Java 17. Se a máquina tiver apenas Java 21+, baixe o Temurin 17 e aponte via org.gradle.java.home no android/gradle.properties.
# Crie o local.properties com o caminho do Android SDK
echo "sdk.dir=$HOME/Library/Android/sdk" > android/local.properties
# Build da APK de release
cd android
./gradlew assembleReleaseA APK gerada fica em:
android/app/build/outputs/apk/release/app-release.apk
O arquivo
android/local.propertiesestá no.gitignore— cada desenvolvedor precisa criá-lo localmente.
- Crie o arquivo em
src/screens/NovaTela.tsx - Exporte a função com o mesmo nome do arquivo
- Adicione o tipo ao union
ScreenemuseAppStore.ts - Monte condicionalmente no
AppShelldentro deApp.tsx - Adicione o item de navegação ao
BottomNavse necessário
Toda adição ao store deve seguir o padrão:
// 1. Declare o campo na interface AppState
novoValor: string;
setNovoValor: (v: string) => void;
// 2. Implemente na factory do create()
novoValor: 'default',
setNovoValor: (v) => set({ novoValor: v }),
// 3. Adicione ao partialize() se deve ser persistido
partialize: (state) => ({
...
novoValor: state.novoValor,
})Edite DEFAULT_REMINDERS e REMINDER_TEMPLATES em useAppStore.ts e TimeEditSheet.tsx respectivamente. O campo template: true impede que o usuário exclua o lembrete pela UI.
// Em ALL_BADGES (useAppStore.ts):
{ id: 'novo', kind: 'daily', name: 'Nome', desc: 'Descrição', icon: 'drop' }
// Em computeNewBadges():
if (/* condição */) add('novo');- Nunca use valores de cor ou tamanho hardcoded nos componentes
- Cores: sempre via
c.nomeDoTokendentro domakeStylesoucolors.nomeDoTokenviauseTheme() - Tamanhos:
FontSizes.*,Spacing.*,Radii.*desrc/theme/tokens.ts - Cores da garrafa:
SHADES[bottleColor].nomeDoShadedesrc/theme/colorShades.ts
Sons são gerados sinteticamente em PCM e cacheados como WAV no sistema de arquivos local. Para adicionar um novo som:
// Em sounds.ts
async function buildNovoSom(): Promise<Float32Array> {
// sintetize aqui
}
export async function playNovoSom(): Promise<void> {
const uri = await getUri('novo', buildNovoSom);
await play(uri);
}Após qualquer alteração nos lembretes, chame scheduleReminders(reminders) para reprocessar todos os agendamentos. O handler global é registrado uma única vez em App.tsx (fora de qualquer componente) via setupNotificationHandler().
Se precisar alterar o widget Android:
| Arquivo | Responsabilidade |
|---|---|
CopinhoWidget.kt |
Renderiza o RemoteViews e lê SharedPreferences |
WidgetModule.kt |
Expõe updateWidget(cur, goal) para o JS via @ReactMethod |
WidgetPackage.kt |
Registra o módulo no ReactPackage |
MainApplication.kt |
Adiciona WidgetPackage() à lista de pacotes |
res/layout/copinho_widget.xml |
Layout do widget |
res/xml/copinho_appwidget_info.xml |
Metadados do widget (tamanho, update interval) |