Skip to content

Repository files navigation

GeoRanker

A map-first place discovery app built with Kotlin Multiplatform — where the interesting part isn't the map, it's the ranking and sync system underneath it.


What makes this different from a maps wrapper

Most "nearby places" apps are thin wrappers around a search API. GeoRanker is built around a custom ranking engine that scores places across three weighted signals and generates a human-readable explanation for every recommendation — not just a sorted list.

The ranking formula:

Signal Weight Logic
Rating 50% Higher-rated places rank higher
Distance 30% Closer places get a proximity boost
Price level 20% Budget-friendly options are prioritized

Every result includes an explainability string generated by the domain layer — e.g. "Highly rated (4.8★) and closer than 90% of nearby cafes" — so the user knows why something ranked first, not just that it did.


Architecture

Two modules with a deliberate boundary between them:

composeApp/          → Android UI shell (Compose, Maps, ViewModel)
sync/                → Shared domain, ranking, persistence, sync
  ├── domain/
  │   ├── ranking/RankingEngine.kt       ← pure Kotlin, no Android deps
  │   ├── usecase/GetRankedPlacesUseCase.kt
  │   └── model/  (Place, RankedPlace, AreaInsight, Filters)
  ├── data/
  │   ├── PlaceRepository.kt             ← single source of truth
  │   ├── RemoteDataSource.kt            ← interface, swappable
  │   └── MockDataSource.kt              ← used for dev/testing
  └── db/
      └── Place.sq                       ← SQLDelight schema

The sync module has a jvmMain source set, which means the ranking engine and repository logic run and test on JVM without any Android runtime dependency.


Offline-first sync

Data always comes from the local SQLDelight database. Remote fetches happen asynchronously through SyncManager, coordinated by SyncWorker (WorkManager) on Android.

Conflict resolution is timestamp-based — updatedAt wins. The UI never waits on the network; it renders from cache and updates when sync completes.

UI observes Flow from SQLDelight
    ↑
PlaceRepository (local source of truth)
    ↑               ↑
SQLDelight       SyncManager ← WorkManager (periodic)
                     ↑
               RemoteDataSource

Area Insights

Beyond individual rankings, GeoRanker calculates aggregate signals for the visible map area per category:

  • Average rating across visible places
  • Price distribution (how many budget vs. expensive options)
  • Exposed as AreaInsight from the domain layer, not derived in the UI

This means the summary panel reflects the actual ranked dataset, not a separate API call.


Tech Stack

Layer Choice
Language Kotlin 2.0+
UI Jetpack Compose Multiplatform
State Coroutines + Flow
Persistence SQLDelight
Maps Google Maps SDK (Android)
Background sync WorkManager
DI Constructor injection (no framework)
Build Gradle Kotlin DSL

No Retrofit. No Hilt. The network boundary is abstracted behind RemoteDataSource — swapping in a real API requires implementing one interface.


Running locally

  1. Add your Maps API key to local.properties:
    MAPS_API_KEY=YOUR_KEY_HERE
    
  2. Requires JDK 17+ and Gradle 8.x.
# Android
./gradlew :composeApp:installDebug

# JVM desktop
./gradlew :composeApp:run

What to look at first

If you're reading this as a code reference:

  • Ranking logic: sync/src/commonMain/domain/ranking/RankingEngine.kt
  • Offline sync flow: sync/src/commonMain/data/SyncManager.kt
  • Explainability: how RankedPlace carries human-readable reasoning from domain → UI
  • KMP boundary: expect/actual in Platform.kt and DatabaseDriverFactory

MIT License LICENCE.md

About

Map-first place discovery built with KMP. Custom ranking engine scores places by rating, distance, and price with explainable recommendations. Offline-first, WorkManager sync, SQLDelight. No API required to run.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages