Native Kotlin/Jetpack Compose client for the Weather API Aggregator — proves the same backend contract that powers the web and iOS clients serves Android too.
Live demo: not published (no Play Store account for this project) — can also point at the live backend directly, see How to Run.
One of three clients (Web / iOS / Android) built on top of the same backend. This app talks directly to the Weather API — it never talks to Open-Meteo/OpenWeatherMap directly.
- 🔎 City search with debounced autocomplete (backend geocoding endpoint) — also used by "add favorite," so a favorite can only ever be a real geocoded place, never unvalidated free text
- 🌡️ Current weather + hourly/daily forecast chart (hand-rolled Canvas line/bar charts), with a °C/°F toggle
- 📇 Tap a Dashboard card for more — the weather, sea-conditions and "more about today" cards each open a bottom sheet with more detail than fits on the compact card (full stat breakdown, tide list with today's swell/fishing/surf context, next-7-days UV/outdoor/fishing/surf outlook)
- 🏠 Home-screen widget (Jetpack Glance) — shows the last weather the app loaded, refreshed whenever the Dashboard loads weather, right after the widget is placed, and every 3 hours in the background (WorkManager)
- ⚡ Cache badge — "dados frescos" vs "servido da cache há Xs", ticking live from the response's
fromCacheflag and timestamp - 🔁 Fallback banner — appears when the response was served by the secondary provider
- 🔐 Auth (register/login, JWT in
EncryptedSharedPreferences), favorite cities (add/remove), search history (delete one entry or clear all), saved unit preference - 🛡️ Admin dashboard — admin accounts get a "Administração" entry in Settings listing every registered account (email, role, joined date) with a per-row delete action (self-delete hidden client-side, refused server-side too)
- ✅ Loading, error and empty states throughout
WeatherApp-Android (Jetpack Compose)
│ Retrofit + OkHttp, Bearer token from EncryptedSharedPreferences — no BFF, talks to the API directly
▼
WeatherAPI (Spring Boot, sibling repo, host machine, reached via 10.0.2.2:8080 from the emulator)
│ cache (Caffeine) → circuit breaker + retry → provider adapters
▼
Open-Meteo / OpenWeatherMap (external providers)
app/src/main/kotlin/dev/ividi/weatherapp/
├── data/
│ ├── model/ # kotlinx.serialization data classes mirroring the backend DTOs exactly
│ ├── network/ # Retrofit service, AuthInterceptor, error-body parsing → typed ApiException
│ ├── auth/ # EncryptedSharedPreferences-backed token storage
│ └── repository/ # incl. WeatherWidgetRepository — DataStore snapshot for the home-screen widget
├── ui/ # one ViewModel (StateFlow) + Composable screen per feature:
│ # auth, dashboard (search, weather card, cache badge, fallback banner, forecast chart,
│ # tap-for-detail bottom sheets), favorites, history, settings, admin (admin-only)
├── widget/ # Jetpack Glance home-screen widget + its WorkManager background refresh
└── di/ # Hilt modules
- Direct-to-API, no BFF: like the iOS client (and unlike the web client, which proxies through Next.js to keep the JWT out of browser JS),
EncryptedSharedPreferencesis already a secure, sandboxed place to hold a token on-device — no need for a server-side proxy layer. 10.0.2.2instead oflocalhost: the Android emulator runs in its own network namespace:10.0.2.2is Google's documented alias back to the host machine'slocalhost. Anetwork_security_config.xmlcleartext exception is needed for it too, since Android blocks plaintext HTTP by default since API 28.- Hand-rolled Canvas charts over a charting library: there's no Compose charting library as mature as Swift Charts/Recharts; for a simple hourly-line/daily-bar chart, a small custom
Canvascomposable is less risk than pulling in and learning a third-party dependency (KISS/YAGNI). - Local-datetime forecast parsing:
hourly[].time/daily[].datecome back from the API without a timezone offset (Open-Meteo'stimezone=autoalready localizes them), so they're parsed askotlinx.datetime.LocalDateTime/LocalDate, notInstant. - Widget renders a stored snapshot, refreshed by the app: the Glance widget itself never makes a network call. It renders whatever
WeatherWidgetRepositorylast persisted, written byDashboardViewModelafter a successful load and pushed to placed widgets viaGlanceAppWidget.updateAll. So the widget doesn't go stale between app opens,WeatherWidgetRefreshWorkerreplays the same GPS lookup every 3 hours (WorkManager, network required), plus once straight away when the widget is placed.updatePeriodMillis="0"stays on purpose, because WorkManager owns the schedule. ModalBottomSheetfor card detail views: the Dashboard's weather/sea-conditions/insights cards each open aModalBottomSheeton tap rather than a new destination — consistent with this app already using in-place dialogs (e.g. Admin's delete confirmation) instead of extra nav-graph routes for transient, dismissible content.
Prerequisites: JDK 17, Android SDK (API 34+ platform + an emulator system image). By default the app talks to the live Weather API deployment at https://weatherapi-4r5x.onrender.com/ (Render free tier, so the first request after a quiet period can take up to a minute). To use a local backend instead (see that repo's README), change BASE_URL in NetworkModule (http://10.0.2.2:8080/ from the emulator).
export JAVA_HOME=/opt/homebrew/opt/openjdk@17 # or your JDK 17 install
export ANDROID_HOME=$HOME/Library/Android/sdk # or your SDK location
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n dev.ividi.weatherapp/.MainActivityThe default base URL is the live HTTPS API. http://10.0.2.2:8080/ is only for a backend running locally on the emulator's host.
./gradlew test- JSON parsing fixtures for
WeatherResponse/ForecastResponse(pinning the local-datetime hourly/daily parsing),WeatherInsightsResponse/MarineResponse,UserAccount,Units. MockWebServer-backed test of the API client's error-body parsing (non-2xx → typedApiExceptioncarrying the backend'smessage) and token-refresh/retry behavior.- ViewModel tests for the admin user list/delete/error paths, search history, and favorites (incl. the debounced geocoding-suggestion pipeline behind "add favorite").
- Pure-function unit tests for cache-age formatting and fallback-provider detection.
Given the project's scope (three client apps on one backend), test effort is weighted toward parsing/business-logic rather than Compose UI layout.
- The live API works on emulators and physical devices. For local development on a physical device, use a reachable LAN address with the appropriate transport configuration.
- The widget's background refresh needs location permission, which is granted inside the app, so on a fresh install the widget shows a placeholder until the app has been opened once.
MIT — see LICENSE.
Developed by David Arsénio Martins 🌐 ividi.dev · 💻 github.com/VidiPT89