Kotlin Multiplatform app (Android · iOS · macOS · Linux · Windows desktop) that visualizes European statistical data from the official Eurostat public API. Eight thematic modules — economy, population, environment, trade, transport, tourism, social, science — each with its own dataset, switcher pattern, and signature chart.
Disclaimer. This is an independent, third-party open-source mobile client for the public Eurostat HTTP API. It is not affiliated with, endorsed by, or sponsored by Eurostat, the European Statistical Office, or the European Commission. The name "Eurostat" appears throughout this project solely to reference the public data source. The app is published under the AGPL-3.0 license and the underlying data is published by Eurostat under Creative Commons Attribution 4.0.
Phone · tablet · desktop — single codebase, three form factors. Android handles phone + tablet via responsive layouts; iOS does iPhone + iPad; the JVM/Compose Desktop target packages a self-contained .jar (~99 MB) that runs on macOS/Linux/Windows.
Status: Phase 4 complete · all 8 modules wired end-to-end to real public-API data · Android APK assembles · iOS Xcode wrapper project is currently a placeholder stub (KMP common code compiles for iOS targets but the wrapper needs to be regenerated — see NEXT_STEPS.md P0-2).
Known limitations: no Android app icon yet (default robot), no release signing config, iOS Xcode wrapper project needs regeneration. See NEXT_STEPS.md for the full roadmap.
See CLAUDE.md for the project conventions and the Eurostat dataset/filter table, and NEXT_STEPS.md for the prioritized roadmap.
License: AGPL-3.0-or-later (see LICENSE).
Eight thematic modules, one app, real public-API data — open any tab, pick a country, scrub a year.
A demographic pyramid for 18 age cohorts (five-year bands, Y_LT5 through Y_GE85) from the demo_pjangroup dataset. The headline shows total population with year-over-year change. A three-option segmented control (Total / Men / Women) dims the opposite side of the pyramid when a single sex is selected. A year slider scrubs through the available observation years; a country chips row switches the active country without re-fetching. A "+" chip opens a searchable country picker to add more countries to the local cache.
Three macro indicators — GDP in current prices (billion EUR, nama_10_gdp), HICP inflation index (2015 = 100, prc_hicp_aind), and government net lending/borrowing as % of GDP (gov_10dd_edpt1) — displayed as a multi-country line chart. A segmented control (GDP / Inflation / Deficit) switches the active metric; the two inactive metrics remain visible as dimmed secondary stat tiles. A dual-handle year scrubber trims the chart's x-range; a separate year dropdown picks the year used by the headline and stat tiles. Country chips and a country picker control which series appear on the chart.
Three climate datasets selectable via a dropdown (GHG / Energy / SDG): greenhouse gas emissions in Mt CO₂-eq (env_air_gge), final energy consumption in ktoe (nrg_bal_c), and the SDG 13 climate index at 1990 = 100 (sdg_13_10). When GHG or Energy is active, a chip row filters by sector (TOTAL / TRANSPORT / INDUSTRY). A year dropdown next to the metric dropdown picks which year the headline reports; the chart keeps the full history. The hero is a line chart comparing the active country against one peer; two secondary stat tiles show the other two metrics' values for the same year. A country chips row and picker control the comparison set.
Intra-EU goods trade flows from ext_lt_intratrd — exports, imports, and trade balance in billion EUR. The hero is a diverging bar chart spanning the eight most recent years, with exports extending to the right and imports to the left. Three underline tabs (Exports / Imports / Balance) highlight the relevant direction. A year dropdown picks the year shown in the headline and stat tiles. Two compact stat tiles show the active country's exports and imports figures for that year. Country chips switch the active country; the "+" picker adds more.
Road and air passenger volumes presented as small-multiple line charts side by side. Road data (billion passengers, road_pa_buscoa) and air data (million passengers, avia_paoc) are independently scaled per panel. A pill toggle (ROAD / AIR / ALL) controls which panels are visible. A log-scale switch on the same row compresses the axis for countries with very different magnitudes. A year dropdown picks the year for the headline and stat tiles; because road and air datasets have different latest years, the dropdown lists the union — picking a road-only year shows "—" in the air tile, and vice versa. Three stat tiles summarise road, air, and a "sea: n/a" tile that explains the port-keyed dataset is intentionally excluded. Country chips switch the active country.
Accommodation overnight stays from tour_occ_ninat — domestic, foreign, and total nights — rendered as a stacked bar chart covering the nine most recent years. A chip row (Domestic / Foreign / Total) controls which metric drives the headline figure; a year dropdown next to it picks the year. Below the stacked bars, a monthly seasonality heatmap (cells = month × year) shows the distribution of nights through the calendar using tour_occ_nim data; the heatmap is independent of the headline year and always shows the five-year window. A shape-preserving placeholder is shown while data loads. Country chips and a picker switch the active country.
Three welfare percentage indicators drawn from EU-SILC surveys: at-risk-of-poverty rate (ilc_li02), at-risk-of-poverty-or-social-exclusion rate (ilc_peps01), and share of population reporting very-good self-perceived health (hlth_silc_01). All three are plotted together as a multi-line highlighted chart; tapping a KPI tile (poverty / at-risk / health) shifts the emphasis to that series and updates the headline value. A year scrubber trims the chart's x-range; a separate year dropdown picks the year used by the headline and KPI tiles. Country chips and a picker switch the active country.
Three innovation indicators — R&D expenditure as % of GDP (rd_e_gerdtot), internet usage rate (isoc_ci_ifp_iu), and tertiary education attainment among 25–64 year-olds (edat_lfse_03) — shown simultaneously on a radar chart. The radar overlays the active country against a single peer (preferring DE, then FR, then EU27). A year dropdown picks the year drawn by the radar; the three sparklines beneath always show the full trend. There is no metric switcher; the radar presents all three axes at once. Country chips switch the active country.
| Capability | Status | Where |
|---|---|---|
| Real Eurostat data — all 8 modules | done | feature-*/src/.../data/ — live API calls, no hardcoded mocks (tourism seasonality heatmap uses live tour_occ_nim data) |
| Stale-while-revalidate caching | done | core-database/ + repository contract in CLAUDE.md |
| Offline support (cached data survives no-network) | done | StaleBanner + OfflineBanner in core-ui/component/states/ |
| Country chips row — switch active country without re-fetch | done | CountryChipsRow in core-ui/component/ |
| Searchable country picker — add countries to session | done | CountryPickerSheet in core-ui/component/ |
| Year scrubber / slider — trim or scrub time range | done | YearScrubber (range) + Slider (Population) in core-ui/ |
| Year picker — single-year dropdown on every module's headline | done | YearDropdown in core-ui/component/ — pick any historical year, headline and stat tiles update without re-fetching |
| 8 chart types on pure Compose Canvas | done | core-charts/ — line, stacked-bar, pyramid, heatmap, diverging-bar, radar, small-multiples, multi-line-highlighted |
| Responsive layout — phone, tablet, desktop | partial | Compact / Medium / Expanded breakpoints in core-ui/layout/; explicit tablet master-detail layouts planned — see NEXT_STEPS.md |
| Multi-country comparison overlay | planned | NEXT_STEPS.md Phase 5 |
| Overview dashboard screen | planned | NEXT_STEPS.md Phase 5 — BottomTabDestination.Overview wired but screen empty |
| Search & Settings screens | planned | NEXT_STEPS.md Phase 5 |
| Real flag rendering | planned | NEXT_STEPS.md Phase 5 |
| iOS builds verified on simulator | planned | NEXT_STEPS.md P0-2 — xcrun exits 72; KMP common code compiles for iOS targets |
| Android verified on physical device | planned | NEXT_STEPS.md P0 smoke-test |
Captured from the running Android build. Full gallery in docs/screenshots/ (34 frames in chronological capture order — Population pyramid → Economy GDP switcher → Environment sector chips → Trade diverging bars → Transport small-multiples → Tourism stacked bars → Social KPI tiles → Science radar → states).
For the visual design language (typography, spacing, color tokens, switcher patterns), open design/index.html in any browser — the wireframes that drove the implementation.
# Android
./gradlew :composeApp:assembleDebug
# → composeApp/build/outputs/apk/debug/composeApp-debug.apk
# Install on a connected device / emulator
./gradlew :composeApp:installDebugiOS: the iosApp/iosApp.xcodeproj is currently a placeholder. To enable iOS builds, regenerate the project via kotlin("multiplatform") Xcode integration or cocoapods plugin, then open in Xcode 15+ and run on a simulator. Tracked in NEXT_STEPS.md P0-2.
# Desktop (macOS / Linux / Windows)
./gradlew :composeApp:packageUberJarForCurrentOS
# → composeApp/build/compose/jars/composeApp-{os}-{arch}-1.0.0.jar (self-contained, ~99 MB)
java -jar composeApp/build/compose/jars/composeApp-*.jar
# OR run in dev mode
./gradlew :composeApp:desktopRunTablet: same APK / iOS app — the Compose UI is responsive. Phone-layout currently primary; explicit tablet layouts (wide-screen master-detail) on the roadmap (see NEXT_STEPS.md).
Tests:
./gradlew allTests # all modules
./gradlew :feature-population:allTests # one moduleMulti-module Clean Architecture. 13 modules; each feature follows the same data/ → domain/ → ui/ layering.
core-common Result<T>, AppError, DispatcherProvider
core-jsonstat JSON-stat 2.0 parser
core-network Shared Ktor HttpClient + EurostatApiClient
core-database SQLDelight schemas + DAOs
core-ui Design system: EurostatTheme + Euro.* tokens + ~25 reusable composables
core-charts 8 chart types on pure Compose Canvas
core-navigation Decompose root component
feature-{population, economy, environment, trade,
transport, tourism, social, science}
Each module owns: ApiService → CellMapper → Cache → Repository
→ Component (Decompose) → UiState → Screen
composeApp App shell: top module switcher + active screen + 5-tab bottom nav
iosApp Xcode wrapper around ComposeUIViewController
Tech stack: Compose Multiplatform · Decompose · Coroutines + Flow + StateFlow · Ktor (Darwin/OkHttp) · kotlinx.serialization · SQLDelight · Koin · pure Compose Canvas charts.
Production aesthetic — calm, editorial, "Apple Health / Linear / Stripe" quality bar. Use through object Euro:
@Composable
fun MyScreen() {
EurostatTheme {
Column(Modifier.background(Euro.colors.paper)) {
ModuleAppBar(
title = "Economy",
accent = Euro.moduleAccents.forModule("Economy"),
onBack = {}, onSearch = {}, onRefresh = {},
)
MetricHeadline(
value = "3 451", unit = "B €",
sub = "GDP · current prices · ▲ +6.2%",
year = "2024",
accent = Euro.moduleAccents.economy,
)
EuroCard {
EurostatLineChart(series = …, xAxis = …, yAxis = …)
}
}
}
}8 module accents (Euro.moduleAccents.economy/people/climate/trade/transport/tourism/social/science) — all desaturated, same chroma, hue-shifted.
Repository returns Flow<Result<T>> with stale-while-revalidate:
- emit
Loading - if cache exists → emit
Success(data, isStale=true) - fetch network → on success emit
Success(data, isStale=false)+ update cache - on network failure: emit
Erroronly if no cached data was emitted
Never throw across the data/domain boundary. Always map to AppError.
No Dispatchers.IO/Default directly — inject DispatcherProvider and use dispatchers.io.
No !! anywhere — use requireNotNull, checkNotNull, or sealed-class exhaustive matching.
See CLAUDE.md for the full contract reference and the Eurostat dataset/filter table (the source of truth for every ApiServiceImpl's buildFilters()).
- Copy
feature-population/as a template. - Replace
PopulationDataPoint/PopulationTimeSerieswith your domain model. - Set dataset code + filters in
XxxApiServiceImpl.buildFilters()— cross-check against the table inCLAUDE.md. - Adapt
JsonStatCell → YourDomainin theCellMapper. - Pick the switcher pattern that fits your data shape (segmented / chip row / underline tabs / pill toggle / metric dropdown / KPI tile selector / none).
- Build the Screen using
Euro.*tokens + a chart fromcore-charts/. - Write tests in
commonTest/(kotlin.test + Turbine). Fake the repository directly — Mockk doesn't work on iOS. - Register the module in
composeApp/.../EurostatApp.kttabs andcore-navigation/.../ChildConfig.kt.
| File | Purpose |
|---|---|
README.md |
This file — orientation for humans. |
CLAUDE.md |
Project conventions + Eurostat dataset/filter table (the contract). Read by AI coding assistants and human contributors alike. |
CONTRIBUTING.md |
How to set up dev env, code style, PR process. |
SECURITY.md |
Vulnerability reporting policy. |
CODE_OF_CONDUCT.md |
Contributor Covenant 2.1. |
NEXT_STEPS.md |
Prioritized roadmap to 1.0. |
design/ |
HTML/JSX wireframes that drove the UI design. Reference only — not production. |








