Skip to content

Repository files navigation

🤖 WeatherApp — Android Client

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.

📦 What's Inside

  • 🔎 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 fromCache flag 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

🛠️ Tech Stack

Kotlin Jetpack Compose Material 3 Glance Hilt Retrofit JUnit

🏗️ Architecture

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

Why these choices

  • 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), EncryptedSharedPreferences is already a secure, sandboxed place to hold a token on-device — no need for a server-side proxy layer.
  • 10.0.2.2 instead of localhost: the Android emulator runs in its own network namespace: 10.0.2.2 is Google's documented alias back to the host machine's localhost. A network_security_config.xml cleartext 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 Canvas composable is less risk than pulling in and learning a third-party dependency (KISS/YAGNI).
  • Local-datetime forecast parsing: hourly[].time/daily[].date come back from the API without a timezone offset (Open-Meteo's timezone=auto already localizes them), so they're parsed as kotlinx.datetime.LocalDateTime/LocalDate, not Instant.
  • Widget renders a stored snapshot, refreshed by the app: the Glance widget itself never makes a network call. It renders whatever WeatherWidgetRepository last persisted, written by DashboardViewModel after a successful load and pushed to placed widgets via GlanceAppWidget.updateAll. So the widget doesn't go stale between app opens, WeatherWidgetRefreshWorker replays 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.
  • ModalBottomSheet for card detail views: the Dashboard's weather/sea-conditions/insights cards each open a ModalBottomSheet on 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.

🚀 How to Run

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/.MainActivity

The 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.

✅ Tests

./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 → typed ApiException carrying the backend's message) 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.

📝 Notes

  • 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.

📄 License

MIT — see LICENSE.


Developed by David Arsénio Martins 🌐 ividi.dev · 💻 github.com/VidiPT89

About

Native Kotlin/Jetpack Compose client for the Weather API — cache badge, fallback banner and provider comparison, built with Hilt and Retrofit.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages