# Notas — cliente Android (offline-first) Cliente mobile da plataforma [notas-online](https://git.labexposed.com/lgcosta/notas-online) (editor de notas estilo Mousepad com versionamento Git-like). **Kotlin + Jetpack Compose**, arquitetura offline-first: tudo funciona sem rede, com sincronização bidirecional em segundo plano (WorkManager) e resolução determinística de conflitos. ## Stack (e por que é a mais leve das opções) | Requisito | Escolha | Peso | |---|---|---| | UI | Jetpack Compose (Material 3, dynamic color) | nativo, sem engine extra | | Banco local | **Room** (SQLite) + WAL | ~0 APK extra relevante vs Flutter/RN | | Fila offline | tabela `outbox` (FIFO por `seq`) | — | | Sync em background | **WorkManager** (periódico 1h + `runNow` por gatilhos) | — | | Rede | OkHttp (timeout 10/20s, retry 2x com backoff em `IOException`) | — | | JSON | kotlinx.serialization (compile-time, sem reflection) | — | | Config | DataStore Preferences | — | | Conflitos | `base_hash` da API (409) + política *servidor vence + cópia local* | — | Flutter (engine ~8 MB) e React Native (bridge/Hermes) dariam mais runtime pelo mesmo trabalho; Android já entrega exatamente as primitivas que offline-first pede (Room, WorkManager, ConnectivityManager). ## Preparação do ambiente — Fedora 44 ```bash # 1) Java 21 (AGP 8.7 trabalha com 17/21) + utilitários sudo dnf install -y java-21-openjdk-devel git unzip curl findutils # 2a) Caminho recomendado: Android Studio (SDK + emulador inclusos) # baixe o .tar.gz em https://developer.android.com/studio e: mkdir -p ~/opt && tar -xzf ~/Downloads/android-studio-*.tar.gz -C ~/opt ~/opt/android-studio/bin/studio.sh # primeiro assistente instala o SDK em ~/Android/Sdk # 2b) Caminho só-CLI (sem Studio): mkdir -p ~/Android/Sdk/cmdline-tools && cd /tmp curl -LO https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip unzip -q commandlinetools-linux-*.zip -d ~/Android/Sdk/cmdline-tools mv ~/Android/Sdk/cmdline-tools/cmdline-tools ~/Android/Sdk/cmdline-tools/latest yes | ~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager --licenses ~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager \ "platform-tools" "platforms;android-35" "build-tools;35.0.0" # 3) ~/.bashrc (ou ~/.zshrc) export JAVA_HOME=/usr/lib/jvm/java-21-openjdk export ANDROID_HOME=$HOME/Android/Sdk export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools ``` O projeto usa o **Gradle Wrapper** (8.11.1) — não é preciso instalar Gradle. Se clonar sem os jars do wrapper, gere-os uma vez: `gradle wrapper --gradle-version 8.11.1` (ou abra no Android Studio, que regenera automaticamente). ## Estrutura do repositório ``` notas-online-android/ ├── settings.gradle.kts / build.gradle.kts / gradle.properties ├── gradle/wrapper/ # Gradle 8.11.1 ├── app/ │ ├── build.gradle.kts # AGP 8.7.3 · Kotlin 2.0.21 · Compose BOM 2024.12 │ ├── proguard-rules.pro │ └── src/main/ │ ├── AndroidManifest.xml # INTERNET + ACCESS_NETWORK_STATE; cleartext p/ LAN │ ├── res/ # tema, ícone adaptativo │ └── java/com/labexposed/notas/ │ ├── NotasApp.kt # Application: periodic work + gatilho de rede │ ├── MainActivity.kt # NavHost + SyncBanner (estado da sincronização) │ ├── data/ │ │ ├── local/ # Room: notes, outbox, categories, meta │ │ └── remote/ # ApiClient (OkHttp+DTOs), ConnectivityObserver │ ├── domain/ # SyncEngine, SettingsRepository, modelos de estado │ ├── sync/ # SyncWorker + SyncScheduler (WorkManager) │ └── ui/ # ViewModel, telas (Settings/Lista/Editor), tema ``` ## Como funciona a sincronização 1. **Tudo é local primeiro**: editar grava no Room e marca a nota `dirty`; a mutação entra na `outbox` (fila FIFO). Notas criadas offline ganham id `L…` temporário. 2. **Push**: para cada item da fila — `create` (POST, renomeia o id local para o id do servidor), `update` (PUT com `base_hash` = último hash conhecido) e `delete` (DELETE; nota local-only só sai do banco). 3. **Conflito (HTTP 409)**: o servidor mudou desde a sua base. Política determinística: **servidor vence a nota + texto local preservado** numa cópia *"Título (conflito)"* (fila como `create`). A versão antiga do servidor também está no histórico de revisões. Nada é perdido e não depende de relógios. 4. **Pull**: categorias + lista de notas; notas locais **limpas** com hash divergente são rebaixadas (GET completo); notas que sumiram do servidor (e não estão na fila) são removidas localmente. 5. **Gatilhos**: WorkManager periódico (1h, exige rede, backoff exponencial) · rede restabelecida (`ConnectivityManager` callback) · após cada edição com conteúdo alterado · botão **Sincronizar** · troca de configuração. 6. **Autosave do editor**: grava local + enfileira após **10 s de ociosidade** (cada tecla reinicia a contagem) — igual à política web. ## Telas - **Servidor**: URL da API + token (`NOTES_TOKEN`) + autor; botão *Testar conexão* (lista notas via `GET /api/notes`) antes de salvar. - **Lista**: banner de status colorido — cinza *não configurado*, cinza-escuro *Offline (N na fila)*, azul *Sincronizando…*, verde *Sincronizado às HH:mm · N pendente(s) · ⚠ conflitos*, vermelho *erro/401*; itens mostram ● não enviado / 🗑 fila / ⚠ nota de conflito. - **Editor**: título, chips de formato (TXT/MD/HTML), chips multi-seleção de categorias (árvore espelhada do servidor), corpo monoespaçado; autosave ocioso + botão Salvar. ## Compilar e instalar ```bash # CLI (debug APK assinado com debug key): ./gradlew assembleDebug # saída: app/build/outputs/apk/debug/app-debug.apk adb install -r app/build/outputs/apk/debug/app-debug.apk # release (R8/minify): ./gradlew assembleRelease # Android Studio: abrir a pasta, sincronizar, Run ▶ (ou Build > Build APK(s)). ``` ## Testado em - **Build**: `assembleRelease` limpo (JDK 21 / AGP 8.7.3 / Kotlin 2.0.21 / SDK 35), APK assinado com R8 ≈ **1,6 MB** (debug ≈ 17 MB). - **Emulador Android 15 (x86_64, Pixel 6)** contra o servidor notas-online real, cenários: - configuração de servidor com *Testar conexão* → OK; - criar nota offline → autosave por ociosidade (10 s) → `POST` único (sem duplicatas); - editar offline → fila resiste à queda de rede → push automático ao restabelecer (`ConnectivityManager`); - nota criada na Web aparece no device via pull; - **conflito real**: device e Web editam a mesma nota → HTTP 409 (`base_hash`) → nota principal adota o servidor e o texto do Android vira exatamente **uma** cópia "… (conflito)" (idempotente; sincronizações serializadas por mutex + índices únicos na outbox). - Integração com a API: mesmos contratos do web client (`base_hash`/409, categorias, revisões). O servidor normaliza `categories`/`tags` vazios como `[]` (null quebrava clientes com tipos não-nuláveis, ex. kotlinx.serialization). ## Roadmap honesto (não implementado aqui) - SSE (`/api/events`) no Android para push tempo-real em vez de gatilho pós-save (OkHttp SSE). - Token em Keystore/EncryptedSharedPreferences (hoje: sandbox do app via DataStore). - Editor WYSIWYG/realce de sintaxe mobile (fora do escopo "mais leve possível"). - Multi-perfil/servidores e sincronização de anexos.