An Android finance dashboard for tracking currency rates and market indicators.
Finance Pulse is a finance dashboard for tracking selected currency pairs and key market indicators in real time.
| Area | Technology |
|---|---|
| Language | Kotlin |
| UI | Jetpack Compose |
| Architecture | MVI + Clean Architecture |
| Dependency Injection | Metro |
| Networking | Ktor |
| Local Storage | DataStore |
| Async | Coroutines + Flow |
| Logging | Timber |
| Build System | Gradle Kotlin DSL + Version Catalog |
| Min SDK | 28 (Android 9.0) |
Metro was chosen as a Kotlin-first compile-time DI solution well-suited for a KMP-oriented project. It provides safe dependency injection without relying on the KAPT/KSP-heavy approaches typical of Hilt or Dagger, making it a more modern and Kotlin-native choice.
The project follows Clean Architecture with a strict dependency rule:
presentation → domain ← data
domain— models, repository interfaces, interactors. Zero external dependencies.data— repository implementations, Ktor remote sources, DataStore local cache.feature— Compose UI screens and ViewModels.core/ui— design tokens, shared Compose components.
finance-pulse/
├── app/ # Application shell, DI graph root
├── shared/
│ ├── domain/ # Domain models, repository interfaces, interactors
│ └── data/ # Data sources, repository implementations
├── core/
│ └── ui/ # Shared Compose components, design tokens
└── feature/
└── dashboard/
├── api/ # Navigation contract (kotlin-jvm)
└── impl/ # Screen, ViewModel, MVI contracts
Each screen follows a strict MVI contract:
// Intent — what the user can do
sealed interface DashboardIntent {
data object Refresh : DashboardIntent
data class InstrumentClicked(val instrumentId: String) : DashboardIntent
}
// State — what the UI renders
data class DashboardUiState(
val isLoading: Boolean = true,
val isRefreshing: Boolean = false,
val lastUpdatedText: String? = null,
val dataStatus: DashboardDataStatus = DashboardDataStatus.FRESH,
val watchlistItems: List<WatchlistItemUiModel> = emptyList(),
val keyIndicators: List<KeyIndicatorUiModel> = emptyList(),
val isOnline: Boolean = true,
val errorMessage: String? = null
)
// Effect — one-time events
sealed interface DashboardEffect {
data class ShowError(val message: String) : DashboardEffect
data class NavigateToInstrumentDetails(val instrumentId: String) : DashboardEffect
}| Source | Data | Auth |
|---|---|---|
| Frankfurter | Currency exchange rates | None |
| CoinGecko | Bitcoin, Ethereum prices | None |
| Yahoo Finance | S&P 500, Gold futures | None |
All sources are free and require no API key.
Default currency watchlist: EUR/USD · EUR/GBP · EUR/JPY · EUR/CHF
Key indicators: S&P 500 · Gold · Bitcoin · Ethereum
The app is resilient to partial failures — if one source is unavailable, the screen still renders with data from the remaining sources.
The dashboard handles all meaningful states:
| State | Description |
|---|---|
| Loading | Initial data fetch in progress |
| Content | Data displayed successfully |
| Refreshing | Pull-to-refresh or manual refresh |
| Error | Network or API failure, no cached data |
| Cached | Showing last known data, network unavailable |
- Dashboard screen
- Currency watchlist with live rates and change %
- Key indicators (S&P 500, Gold, Bitcoin, Ethereum)
- Last updated timestamp
- Manual refresh and pull-to-refresh
- All UI states (loading / content / refreshing / error / cached)
- Frankfurter API integration via Ktor
- CoinGecko + Yahoo Finance API integration
- Partial failure resilience
- Network connectivity monitoring
- Local cache via DataStore
- MVI state management
- Metro DI
- Timber logging
- Unit tests for interactors and mappers
- Clone the repository
- Open in Android Studio Meerkat or newer
- Run on emulator or device with API 28+
No API keys required.