Architecture & System Design
Geotify is built following Android Clean Architecture guidelines and the MVVM (Model-View-ViewModel) design pattern. This architecture ensures strict separation of concerns, high testability, and unidirectional data flow (UDF).
Tech Stack Overview
| Layer / Subsystem | Technology | Purpose & Usage |
|---|---|---|
| Language | Kotlin | Idiomatic Kotlin codebase utilizing Coroutines and Flows for async operations. |
| UI Framework | Jetpack Compose | Declarative UI with Material Design 3 components and dynamic themes. |
| Dependency Injection | Dagger Hilt | Compile-time dependency injection container. |
| Database & Persistence | Room & DataStore | SQLite ORM (geotify.db) for entities; Preferences DataStore for settings. |
| Map Rendering | osmdroid | OpenStreetMap tile rendering, custom marker graphics, and geofence overlays. |
| Location & Geofencing | Google Play Services | FusedLocationProviderClient for GPS and GeofencingClient for background triggers. |
| Background Scheduling | Jetpack WorkManager | Expedited job scheduling for sliding window spatial recalculations. |
| AI Assistant API | Jetpack AppFunctions | Structured background API surface for on-device LLMs (androidx.appfunctions). |
Architecture Layers
+----------------------------------+
| Presentation Layer |
| Jetpack Compose + ViewModels |
+----------------+-----------------+
|
v
+----------------------------------+
| Domain Layer |
| Use Cases & Business Logic |
+----------------+-----------------+
|
v
+----------------------------------+
| Data Layer |
| Repositories, Room DB, DataStore |
+----------------+-----------------+
|
v
+----------------------------------+
| System / GMS |
| GMS Geofencing, WorkManager |
+----------------------------------+
Presentation Layer
- Components: Compose screens (
RemindersScreen,LocationsScreen,SettingsScreen) and reusable components (LocationMapView,MapPicker). - ViewModels:
RemindersViewModel,LocationsViewModel,SettingsViewModel. Expose UI states viaStateFlowand handle user interactions.
Domain Layer
- Encapsulates business logic into standalone Use Cases:
SpatialSearchUseCase: Bounding box filtering and distance sorting for active geofences.SaveCurrentLocationUseCase: High-accuracy location capture and persistence.CreateReminderUseCase: Validating and creating geofenced triggers.DeleteLocationUseCase&DeleteReminderUseCase: Entity cleanup and active fence updates.
Data Layer
- Repositories:
LocationRepositoryandReminderRepositoryact as single sources of truth. - Data Access Objects (DAOs):
LocationDaoandReminderDaoperform Room SQLite queries. - DataStore:
SettingsManagermanages preferences such as outer radiusN, inner radiusr, recalculation debounce delays, and theme modes.
Component Interaction Flow
flowchart TD
subgraph UI ["Presentation Layer"]
Screen[Compose Screen]
VM[ViewModel]
end
subgraph AI ["AI / Assistant Integration"]
AppFunc[AppFunctions Interface]
end
subgraph Domain ["Domain Layer"]
UC[Use Cases / SpatialSearchUseCase]
end
subgraph Data ["Data Layer"]
Repo[Location / Reminder Repository]
Room[(Room Database)]
DS[(Settings DataStore)]
end
subgraph Background ["Background Execution"]
Orchestrator[GeofenceOrchestrator]
WM[WorkManager / GeofenceRecalculationWorker]
Receiver[GeofenceBroadcastReceiver]
GMS[Google Play Services GeofencingClient]
end
Screen -->|User Action| VM
AppFunc -->|System Action| UC
VM -->|Executes| UC
UC -->|Queries / Mutates| Repo
Repo --> Room
Repo --> DS
Repo -->|Trigger Recalculation| Orchestrator
Orchestrator -->|Enqueue Expedited Work| WM
WM -->|Fetch Candidates| UC
WM -->|Register Geofences| GMS
GMS -->|Geofence Transition Event| Receiver
Receiver -->|Master Exit Trigger| Orchestrator
Receiver -->|POI Transition| Repo
OpenStreetMap (osmdroid) Integration
Geotify embeds osmdroid's Android MapView inside Jetpack Compose via AndroidView.
Custom Dark Theme Tile Color Filtering
To render dark-mode map tiles without custom tile servers, Geotify applies a programmatic ColorMatrixColorFilter to the map tile layer:
if (isDarkTheme) {
val filter = ColorMatrixColorFilter(ColorMatrix(floatArrayOf(
-0.1491f, -0.5005f, -0.0504f, 0f, 215f,
-0.1491f, -0.5005f, -0.0504f, 0f, 215f,
-0.1491f, -0.5005f, -0.0504f, 0f, 230f,
0f, 0f, 0f, 1f, 0f
)))
map.overlayManager.tilesOverlay.setColorFilter(filter)
} else {
map.overlayManager.tilesOverlay.setColorFilter(null)
}
Compose Map Lifecycle Handling
Calling MapView.onDetach() during transient composable recompositions or visibility toggles (e.g., AnimatedVisibility) permanently destroys osmdroid's background tile writer thread.
Geotify binds MapView.onDetach() strictly to ON_DESTROY of the host ActivityLifecycleOwner, calling only MapView.onPause() during composition disposal:
DisposableEffect(mapViewRef, lifecycle) {
val map = mapViewRef ?: return@DisposableEffect onDispose {}
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> map.onResume()
Lifecycle.Event.ON_PAUSE -> map.onPause()
Lifecycle.Event.ON_DESTROY -> map.onDetach()
else -> {}
}
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
map.onPause() // Do NOT call map.onDetach() here!
}
}