A minimalist, ultra-lightweight Android currency converter focused on speed, clean UI, and real-time exchange rates. Designed for offline-first use — ideal when travelling with limited connectivity.
- Real-time exchange rates — fetches live data from the ExchangeRate-API (161+ currencies, no API key required)
- Offline support — rates are cached to disk and survive app restarts; the last known rates are always available even without a connection
- Smart caching — in-memory cache with a 1-hour TTL; network is only hit when rates are stale
- Cross-rate conversion — all pairs calculated via a stable EUR base, so any currency combination works
- Searchable currency dropdown — tap the field and type to filter; keyboard opens immediately
- Input validation — non-numeric input is flagged inline; stale results are cleared on each change
- Dark mode — toggle in the app header; preference is persisted across sessions
- "Rates updated X min ago" — timestamp shown below the result so you always know how fresh the data is
- Swap button — swaps From/To currencies in one tap
| Layer | Technology |
|---|---|
| Language | Kotlin 2.x |
| UI | Jetpack Compose + Material 3 |
| Architecture | MVVM (ViewModel + StateFlow) |
| Networking | Ktor Client |
| Serialization | kotlinx.serialization |
| Persistence | SharedPreferences |
| Concurrency | Kotlin Coroutines |
| Min SDK | API 24 (Android 7.0) |
| Target SDK | API 35 (Android 15) |
app/
└── src/main/java/com/minimaexchange/app/
├── MainActivity.kt # Single-activity entry point
├── data/
│ ├── local/
│ │ ├── AppPreferences.kt # Dark mode persistence (SharedPreferences)
│ │ └── RatesCache.kt # Exchange rate disk cache (SharedPreferences + JSON)
│ ├── model/
│ │ └── ExchangeRateResponse.kt # API response model
│ └── remote/
│ ├── ExchangeRateApi.kt # Ktor HTTP client, fetches EUR-based rates
│ └── HttpClientFactory.kt # Ktor client setup with JSON content negotiation
├── domain/
│ └── ConvertCurrencyUseCase.kt # Caching logic, offline fallback, cross-rate calc
└── ui/
├── screen/
│ ├── ConverterScreen.kt # Compose UI — amount input, dropdowns, result
│ ├── ConverterViewModel.kt # UI state, debounced auto-convert, dark mode toggle
│ └── ConverterViewModelFactory.kt # Manual DI — wires all dependencies
└── theme/
├── Color.kt
├── Theme.kt # Light + dark Material 3 color schemes
└── Type.kt
User input
└─> ConverterViewModel (StateFlow + debounce)
└─> ConvertCurrencyUseCase
├─> In-memory cache (< 1h old) ──> Result
├─> ExchangeRateApi (network) ──> Result + save to RatesCache
└─> RatesCache (disk fallback) ──> Result (offline)
| Scenario | Behaviour |
|---|---|
| Fresh app, online | Fetches rates, saves to disk |
| Rate cache < 1 hour old | Returns in-memory cache, no network call |
| Rate cache > 1 hour old, online | Fetches fresh rates, updates disk cache |
| Rate cache > 1 hour old, offline | Falls back to any cached data (no age limit) |
| No cache at all, offline | Shows "No internet connection" error |
Prerequisites: Android Studio with JDK 21 (the project's gradle.properties points to the Android Studio bundled JDK).
# Debug build
./gradlew assembleDebug
# Install on connected device / emulator
./gradlew installDebug
# Run unit tests
./gradlew test
# Lint
./gradlew lintExchange rates are sourced from open.er-api.com — free tier, no API key, 161 currencies. All rates are fetched with EUR as the base and cross-rates are computed client-side:
result = amount × (toRate / fromRate)