Kai Hirota

Project4 min read

Route42

A Strava-style Android app with workout tracking, social posts, and KD-tree nearest-neighbour search.

Route42 is a Strava-style Android app for recording workouts, sharing routes, following other users, and discovering nearby activity posts. It pairs a native Android client with a separate Spring REST API backed by Firebase/Firestore.

Android app source · Database API source

Route42 app screens: feed, activity tracking, profile, route, and nearby search

Product Scope#

The app was built around a simple athlete social-network model:

  • Record walking, running, and cycling activities.
  • Track route, distance, elapsed time, speed, and calories on an interactive map.
  • Turn a completed activity into a post with hashtags and route data.
  • Follow users, like posts, block users, and view public profiles.
  • Search posts by username, hashtags, and geographic proximity.
  • Schedule posts and likes when the app is offline or when the user wants to delay an action.

The Strava comparison is the right mental model: Route42 is not just a photo feed with locations attached. The core object is a workout route, and social discovery happens around activity posts.

Android Client#

The Android app uses a single-activity architecture: MainActivity owns a fragment container and swaps between feed, profile, search, and map fragments. That made it easier to share user state through ViewModels and keep workout state alive while the user navigated around the app.

The activity-tracking flow uses the fused location provider. While recording, the app requests location updates, appends each point to the route, draws the route as a Google Maps polyline, and recomputes metrics.

Location update

ActiveMapViewModel

route polyline + metrics

BaseActivity(distance, elapsed time, speed, calories)

shareable workout post

Distance is computed by summing point-to-point geographic distance along the route:

D=i=1n1d(pi,pi+1)D = \sum_{i=1}^{n-1} d(p_i, p_{i+1})

where dd is the device/platform distance function over latitude-longitude coordinates.

Backend API#

The companion API exists because the mobile Firestore SDK made some search features awkward:

  • partial or structured text search is limited;
  • multiple arrayContains filters are constrained;
  • boolean OR across filters is not directly ergonomic;
  • all database access from the Android client has to be asynchronous and UI-safe.

The backend exposes REST endpoints for post retrieval, structured search, and nearest-neighbour search. The Android app calls it with Retrofit on a background executor so map/search requests do not block the UI thread.

Route42 architecture diagram

Query Parser#

Route42 supports structured post search such as:

username: moniquechan and hashtags: #love #instarunners #triathlon

or:

(username: moniquechan and hashtags: #love) or username: miranda45

The API tokenizes the query string and builds a binary query tree. Leaf nodes are field filters, while internal nodes are boolean operators. Evaluation is recursive:

AND(left, right) -> intersection(results(left), results(right))
OR(left, right)  -> union(results(left), results(right))

That let the app support richer search semantics than the mobile Firestore query path alone.

The geographic discovery feature uses a custom KD-tree in the backend API. This is the part of the project I would emphasize most now.

The endpoint is:

GET /search/knn?k=50&lat=-35.2809&lon=149.1300

The API loads recent posts, converts each post location into a 2D KD-tree node, and searches for the kk closest posts to the user's current latitude-longitude.

Android map screen
      ↓ Retrofit background call
GET /search/knn?k=50&lat=...&lon=...

Spring controller

KDTree.fromPosts(posts)

findKNearest(k, lat, lon)

nearby posts rendered as map markers

Nearby activity-post search on the map

The KD-tree alternates split dimensions between longitude and latitude. Tree construction uses QuickSelect to choose the median node on the active axis, then recursively builds left and right subtrees:

axis = longitude
  median by longitude
    left subtree: axis = latitude
    right subtree: axis = latitude

For distance ranking, the node uses great-circle distance through a haversine-style calculation. That matters because Euclidean distance over raw latitude-longitude is only a rough local approximation.

An R-tree would also be a natural spatial-index choice for rectangle/range queries, but this implementation used a KD-tree because the app's map view needed nearest-neighbour lookup over 2D points.

API Structure#

Route42 REST API UML

The backend separates controller, service, repository, query parsing, and geosearch code:

  • SearchController handles /search and /search/knn.
  • PostServiceImpl wraps post retrieval and filter operations.
  • Tokenizer and QueryTreeNode parse boolean search strings.
  • KDTree, KDTreeNode, and QuickSelect implement spatial indexing.
  • PostRepositoryImpl talks to Firestore through the server-side SDK.

That split kept the Android client focused on UI and state while the backend owned query semantics and search-specific data structures.

Tradeoffs#

The KD-tree endpoint rebuilt the tree from a bounded set of posts on request:

KDTree tree = KDTree.fromPosts(postService.getMany(200));

That was fine for a course-scale prototype, but it would not be the production architecture. A stronger version would maintain a persistent spatial index, cache it, or move spatial indexing into the database layer. For high write volume or rectangular range queries, an R-tree, geohash index, or purpose-built geospatial database would be worth considering.

The query parser also chose simplicity over optimization. A long chain of OR filters becomes a deep binary tree, and each branch is evaluated recursively. That is easy to understand, but a production search system would normalize the query and push more work into indexed retrieval.

Reflection#

Route42 was valuable because it joined mobile UX with backend data-structure work. The Android side had the usual app concerns: lifecycle, permissions, maps, navigation, background work, and responsive UI. The backend side had a different shape: parsing, set operations, repository boundaries, and spatial nearest-neighbour search.

The result was a full-stack Strava clone prototype where the interesting technical center was not only "record a route," but "make route posts searchable by both text and physical proximity."