Notas — cliente Android (offline-first)

Cliente mobile da plataforma 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

# 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) · a cada edição com autosave (10 s de ociosidade), incluindo mudanças só de título/formato/categorias · ao voltar para o app (ON_RESUME) · botão Sincronizar · troca de configuração. Uma auto-reconciliação a cada rodada enfileira qualquer nota suja sem entrada na fila (recupera estados presos).
  6. Tempo real via SSE (GET /api/events, OkHttp, reconexão com backoff, só em primeiro plano): eventos saved atualizam a UI na hora — nota limpa aberta no editor é recarregada ao vivo; nota suja mostra banner "Mudou no servidor — Recarregar/Manter"; snackbar na lista. Eventos presence desenham 👥N na nota quando outra sessão (app ou navegador web) está com ela aberta.
  7. WYSIWYG e Preview (WebView nativo, sem dependência extra): modo WYSIWYG renderiza o Markdown (via /api/render do servidor) num contenteditable; cada edição é convertida de volta para Markdown por um serializador JS embutido (títulos, negrito/itálico/tachado, listas, citação, código, links, tabelas) e sincronizada — notas .html editam o HTML preservando tags/atributos (scripts/on* sanitizados). Preview é o mesmo WebView não-editável. Barra de ferramentas aplica execCommand (B I S H1 H2 listas ❝ 🔗 código).

UI (Material 3, dynamic color)

  • Lista: MediumTopAppBar, banner de status colorido (Offline/Sincronizando/Sincronizado± pendências/Erro), chips de filtro (Todas / 📁 categoria com caminho da árvore / #tag), cards com título+extensão, tempo relativo, tags, chip de categoria(s) e ícone de status por nota (☁✓ sincronizada, ● âmbar não enviada, 🗑 exclusão na fila, ⚠ conflito/mudança remota, 👥N presença), FAB estendido "Nova".
  • Editor: campo de título na top bar, seletor de modo (Editar / WYSIWYG / Preview), chips de formato (TXT/MD/HTML) e de categorias (multi-seleção), corpo monoespaçado com toolbar de Markdown (B I S H1 H2 • ❝ 🔗 ` { }) que envolve/prefixa a seleção, selo de presença 👥, e horário do último salvamento.
  • Servidor: URL + token + autor com Testar conexão e validação de esquema http(s).

Compilar e instalar

# 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;
    • mudança só de título sincroniza (bug reportado em celular real, corrigido: metadados entram na fila + auto-reconciliação + sync no ON_RESUME);
    • SSE ao vivo: PUT vindo da Web atualiza o editor aberto em ~2 s, sem toque; snackbar na lista; 👥 presença aparece quando outra sessão abre a mesma nota;
    • WYSIWYG: digitar no contenteditable → serialização html→md na ponte JS → autosave → servidor recebe o texto; Preview renderiza o Markdown via /api/render;
    • 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, /api/events, /api/presence). 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)

  • Token em Keystore/EncryptedSharedPreferences (hoje: sandbox do app via DataStore).
  • Sync reativo incremental (o pull compara hashes por nota; milhares de notas pediriam um endpoint de delta no servidor).
  • Multi-perfil/servidores e sincronização de anexos.
S
Description
Cliente Android offline-first (Kotlin + Jetpack Compose) para a plataforma notas-online: Room + outbox FIFO, sync bidirecional via WorkManager, conflitos com base_hash/409 (servidor vence + cópia local), autosave por ociosidade.
Readme
241 KiB
0 Stars 1 Watchers 0 Forks
Languages
Kotlin 100%