This chapter walks through building a simple maps client with RxJava, using it as a realistic, end-to-end example of reactive app design. It starts from first principles of tiled map rendering (square Mercator plane, 256×256 tiles, power-of-two zoom levels, top-left indexing) and establishes a minimal, view-friendly data model: a tile carries just enough information for drawing (position, size, indices). The view is kept intentionally “dumb,” rendering whatever collection of tiles it receives, while the view model emits those collections. The initial scaffolding uses fixed values to verify wiring and contracts, then progressively replaces stubs with reactive computations.
The reactive core introduces a zoom-level source and a map offset that represents panning, driven by touch-drag deltas modeled as an event stream. Offsets are accumulated from movement events and applied functionally to tiles, with stream operators combining the latest zoom and offset into drawable output. To stay efficient, the pipeline computes a viewport rectangle from the current offset, tile size, and dynamic view dimensions, generating only the tiles that intersect the visible area. The result is a composable chain—calculate tiles, offset them, then cull to the viewport—that evolves from hardcoded values to fully reactive inputs without rewriting earlier steps.
With the drawing pipeline in place, the view swaps placeholder tiles for real map images using a dedicated loader that handles fetching and caching, keeping the view model focused on “what” to show rather than “how” to retrieve it. Zoom controls are added as reactive button streams that increment or decrement a behavior-backed zoom level. To make zooming feel natural and enable location targeting, the chapter introduces a center defined in latitude/longitude, converts between geographic and pixel coordinates to derive map offsets, and updates the center in response to drags. The final design emphasizes small, reusable steps, clear separation of data and rendering, and an approach that generalizes to other tiled surfaces, such as high-resolution image viewers.
FAQ
Why build a maps client with RxJava in the first place?Because maps are a great real-world fit for reactive streams: drag and zoom are event streams; visible tiles depend on changing inputs (offset, viewport size, zoom); and separating “what to draw” (data) from “how to draw” (rendering) becomes cleaner. Rx chains also scale as features are added without rewriting existing steps.How are tiles organized on a map?Tiles are square images (commonly 256×256 px) arranged in a grid. At zoom level z, the world is 2^z by 2^z tiles. Indices start at the top-left: (0,0) is the first tile, x increases to the right, y increases downward.What minimal data should the view receive to draw tiles?Give the view a plain collection of Tile objects, each with screenX, screenY, width, height, grid indices (i, n), and later the zoomLevel. The view does not need to know about grids or maps—only what and where to draw.How do you compute tiles for a zoom level?Compute size = 2^zoom. Loop i = 0..size-1 and n = 0..size-1 to create Tile instances. Screen positions come from tileSize and a mapOffset, which shifts all tiles at once.What is mapOffset and how is it used?mapOffset is the pixel offset from the view’s origin to the top-left of the world (tile 0,0). It’s applied to every tile’s screen position and is also used to derive which tile indices are currently visible in the viewport.How is dragging modeled with Rx?Convert touch events into an Observable of Point deltas (dx, dy) that emit while dragging. Accumulate these into a BehaviorSubject mapOffset using operators like withLatestFrom (or scan) so each delta updates the current offset.How do you determine which tiles are visible?Use mapOffset, tileSize, and viewSize to compute a rectangle of tile indices:
- left = floor(-offset.x / tileSize), top = floor(-offset.y / tileSize)
- right = ceil((viewSize.x - offset.x) / tileSize)
- bottom = ceil((viewSize.y - offset.y) / tileSize)
Generate only tiles within this Rect to avoid creating the whole world.Where should map images be loaded: in the view or the view model?Keep the view model focused on which tiles should exist; let the view coordinate a TileLoader that downloads and caches images. This keeps dependencies one-way (VM → View), simplifies lifecycle/resource cleanup, and avoids coupling VM logic to image loading state.How are zoom controls implemented reactively?Use a BehaviorSubject<Integer> zoomLevel with a default. Create two streams from plus/minus clicks, combine each withLatestFrom(zoomLevel) to compute increment/decrement, merge them, and feed results back into zoomLevel. The rest of the chain reacts to zoom changes.How do you keep the map centered and support lat/lng?Track a BehaviorSubject<LatLng> centerCoordinate. Convert lat/lng to pixel coordinates using Mercator helpers (fromLatLngToPoint), then derive mapOffset so that the center aligns with the view’s center. On drag, convert pixel deltas to a new lat/lng and push it to centerCoordinate; the chain recalculates mapOffset accordingly.
pro $24.99 per month
access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!