Skip to main content

Data Flow

This document traces the complete data flow for location tracking and geofencing -- from the React hook layer through the TurboModule bridge, into native services, and back to JavaScript via event emitters.

High-Level Flow​

Start Tracking Flow​

When startTracking() is called, data crosses three boundaries before reaching the location provider.

Step 1: TypeScript API (src/index.tsx)​

The toTrackingOptionsSpec() utility in src/utils/trackingOptionsMapper.ts handles two conversions that Codegen cannot:

  1. Enum-to-string -- LocationAccuracy and NotificationPriority enum values become plain strings.
  2. Object-to-JSON -- notificationActions (capped at 3 items) and notificationOptions are serialized to JSON strings.

Step 2: TurboModule Bridge​

The TrackingOptionsSpec interface crosses the bridge with all values in Codegen-compatible types (strings, numbers, booleans). The native side receives a ReadableMap (Android) or NSDictionary (iOS) and reconstructs the typed options.

Step 3: Native Service Initialization​

  • Android: BackgroundLocationModule parses the ReadableMap into a Kotlin TrackingOptions data class, saves tracking state to Room DB, and starts LocationService as a foreground service.
  • iOS: BackgroundLocation.mm forwards to LocationManagerWrapper.swift, which configures CLLocationManager with the mapped accuracy, distance filter, and background mode settings.

Location Update Flow​

Once tracking is active, location updates flow from the OS through the native layer and back to JavaScript.

Android Event Pipeline​

Android uses SharedFlow singletons to decouple event producers from consumers:

SharedFlowSealed InterfaceProducerConsumer
LocationEventFlowLocationEvent (Update, Error, Warning)LocationEventEmitterBackgroundLocationModule
GeofenceEventFlowGeofenceEvent (Transition)GeofenceEventEmitterBackgroundLocationModule
NotificationActionFlowNotificationActionEvent (ActionClicked)NotificationActionReceiverBackgroundLocationModule

All three flows share the same configuration: replay = 0, extraBufferCapacity = 64, onBufferOverflow = DROP_OLDEST. The non-suspending tryEmit() API ensures producers never block.

BackgroundLocationModule collects from all three flows using coroutine Jobs scoped to moduleScope (SupervisorJob + Dispatchers.Main). Each collected event is forwarded to JavaScript via RCTDeviceEventEmitter.

iOS Event Pipeline​

iOS uses RCTEventEmitter directly. LocationManagerDelegate processes each CLLocation from the didUpdateLocations: callback, writes to Core Data, and emits to JavaScript via sendEvent(withName:body:). There is no intermediate flow layer -- the delegate bridges directly to the React Native event system.

Platform Comparison​

AspectAndroidiOS
Event decouplingSharedFlow singletons (3 flows)Direct delegate-to-emitter
Buffer strategy64-element ring buffer, DROP_OLDESTNo buffer (immediate emit)
Thread modelCoroutine collection on Main dispatcherMain thread delegate callbacks
Producer APINon-suspending tryEmit()Synchronous sendEvent()
Consumer lifecycleCoroutine Jobs tied to moduleScopeRCTEventEmitter managed by React Native

Event Payload Format​

Both platforms emit identical event payloads to JavaScript:

onLocationUpdate​

{
tripId: string;
latitude: number;
longitude: number;
timestamp: number;
accuracy?: number;
altitude?: number;
speed?: number;
bearing?: number;
verticalAccuracy?: number;
speedAccuracy?: number;
bearingAccuracy?: number;
elapsedRealtimeNanos?: number;
provider?: string;
isMocked?: boolean;
}

onLocationError​

{
tripId: string;
type: 'PERMISSION_REVOKED' | 'PROVIDER_ERROR';
message: string;
}

onLocationWarning​

{
tripId: string;
type: 'SERVICE_TIMEOUT' | 'TASK_REMOVED' | 'LOCATION_UNAVAILABLE';
message: string;
}

onNotificationAction​

{
tripId: string;
actionId: string;
}

Storage Flow​

Write Path​

Both platforms use the same batching strategy:

  • Locations are buffered in memory.
  • Flush triggers when the buffer reaches 10 items or a 5-second timer fires.
  • On flush failure, items are re-added to the buffer for retry.

Read Path​

Every read operation calls forceFlush() first to drain pending writes, guaranteeing that the returned data includes the most recent buffered locations.

Geofencing Data Flow​

Registration​

Geofence regions go through three transformations before reaching the platform:

  1. Validation -- validateGeofenceRegion() checks identifier, radius, coordinates, and metadata.
  2. Duplicate check -- Queries active geofences to prevent duplicate identifiers.
  3. Serialization -- serializeGeofenceRegion() converts the typed GeofenceRegion to a JSON string for the bridge.

Transition Events​

Transition events are both persisted (for later retrieval via getGeofenceTransitions()) and emitted in real-time to JavaScript.

Stop Tracking Flow​

The stop token is the critical mechanism that prevents the recovery system from restarting a service that was intentionally stopped. It is set synchronously (commit() on Android, synchronous UserDefaults write on iOS) during stopTracking() and checked at three points in the recovery pipeline.

Recovery Flow​

Android (WorkManager)​

iOS (Significant Location Monitoring)​

Hook Data Flow​

useLocationUpdates​

The hook uses three data sources:

  1. Mount hydration -- One-time getLocations() call on mount to load persisted data.
  2. Live events -- NativeEventEmitter subscription for real-time updates.
  3. Foreground re-hydration -- AppState listener triggers getLocations() when the app returns from background, catching any updates that arrived while the JS bridge was inactive.

Next Steps​