12 KiB
Developer Documentation
This document provides detailed technical information about the DeFlock app architecture, key design decisions, and development guidelines.
Philosophy: Brutalist Code
Our development approach prioritizes simplicity over cleverness:
- Explicit over implicit: Clear, readable code that states its intent
- Few edge cases by design: Avoid complex branching and special cases
- Maintainable over efficient: Choose the approach that's easier to understand and modify
- Delete before adding: Remove complexity when possible rather than adding features
Hierarchy of preferred code:
- Code we don't write (through thoughtful design and removing edge cases)
- Code we can remove (by seeing problems from a new angle)
- Code that sadly must exist (simple, explicit, maintainable)
Architecture Overview
State Management
The app uses Provider pattern with modular state classes:
AppState (main coordinator)
├── AuthState (OAuth2 login/logout)
├── OperatorProfileState (operator tag sets)
├── ProfileState (node profiles & toggles)
├── SessionState (add/edit sessions)
├── SettingsState (preferences & tile providers)
└── UploadQueueState (pending operations)
Why this approach:
- Separation of concerns: Each state handles one domain
- Testability: Individual state classes can be unit tested
- Brutalist: No complex state orchestration, just simple delegation
Data Flow Architecture
UI Layer (Widgets)
↕️
AppState (Coordinator)
↕️
State Modules (AuthState, ProfileState, etc.)
↕️
Services (MapDataProvider, NodeCache, Uploader)
↕️
External APIs (OSM, Overpass, Tile providers)
Key principles:
- Unidirectional data flow: UI → AppState → Services → APIs
- No direct service access from UI: Everything goes through AppState
- Clean boundaries: Each layer has a clear responsibility
Core Components
1. MapDataProvider
Purpose: Unified interface for fetching map tiles and surveillance nodes
Design decisions:
- Pluggable sources: Local (cached) vs Remote (live API)
- Offline-first: Always try local first, graceful degradation
- Mode-aware: Different behavior for production vs sandbox
- Failure handling: Never crash the UI, always provide fallbacks
Key methods:
getNodes(): Smart fetching with local/remote merginggetTile(): Tile fetching with caching_fetchRemoteNodes(): Handles Overpass → OSM API fallback
Why unified interface: The app needs to seamlessly switch between multiple data sources (local cache, Overpass API, OSM API, offline areas) based on network status, upload mode, and zoom level. A single interface prevents the UI from needing to know about these complexities.
2. Node Operations (Create/Edit/Delete)
Upload Operations Enum:
enum UploadOperation { create, modify, delete }
Why explicit enum vs boolean flags:
- Brutalist: Three explicit states instead of nullable booleans
- Extensible: Easy to add new operations (like bulk operations)
- Clear intent:
operation == UploadOperation.deleteis unambiguous
Session Pattern:
AddNodeSession: For creating new nodesEditNodeSession: For modifying existing nodes- No "DeleteSession": Deletions are immediate (simpler)
Why no delete session: Deletions don't need position dragging or tag editing - they just need confirmation and queuing. A session would add complexity without benefit.
3. Upload Queue System
Design principles:
- Operation-agnostic: Same queue handles create/modify/delete
- Offline-capable: Queue persists between app sessions
- Visual feedback: Each operation type has distinct UI state
- Error recovery: Retry mechanism with exponential backoff
Queue workflow:
- User action (add/edit/delete) →
PendingUploadcreated - Immediate visual feedback (cache updated with temp markers)
- Background uploader processes queue when online
- Success → cache updated with real data, temp markers removed
- Failure → error state, retry available
Why immediate visual feedback:
Users expect instant response to their actions. By immediately updating the cache with temporary markers (e.g., _pending_deletion), the UI stays responsive while the actual API calls happen in background.
4. Cache & Visual States
Node visual states:
- Blue ring: Real nodes from OSM
- Purple ring: Pending uploads (new nodes)
- Grey ring: Original nodes with pending edits
- Orange ring: Node currently being edited
- Red ring: Nodes pending deletion
Cache tags for state tracking:
'_pending_upload' // New node waiting to upload
'_pending_edit' // Original node has pending edits
'_pending_deletion' // Node queued for deletion
'_original_node_id' // For drawing connection lines
Why underscore prefix: These are internal app tags, not OSM tags. The underscore prefix makes this explicit and prevents accidental upload to OSM.
5. Multi-API Data Sources
Production mode: Overpass API → OSM API fallback Sandbox mode: OSM API only (Overpass doesn't have sandbox data)
Zoom level restrictions:
- Production (Overpass): Zoom ≥ 10 (established limit)
- Sandbox (OSM API): Zoom ≥ 13 (stricter due to bbox limits)
Why different zoom limits: The OSM API returns ALL data types (nodes, ways, relations) in a bounding box and has stricter size limits. Overpass is more efficient for large areas. The zoom restrictions prevent API errors and excessive data transfer.
6. Offline vs Online Mode Behavior
Mode combinations:
Production + Online → Local cache + Overpass API
Production + Offline → Local cache only
Sandbox + Online → OSM API only (no cache mixing)
Sandbox + Offline → No nodes (cache is production data)
Why sandbox + offline = no nodes: Local cache contains production data. Showing production nodes in sandbox mode would be confusing and could lead to users trying to edit production nodes with sandbox credentials.
Key Design Decisions & Rationales
1. Why Provider Pattern?
Alternatives considered:
- BLoC: Too verbose for our needs
- Riverpod: Added complexity without clear benefit
- setState: Doesn't scale beyond single widgets
Why Provider won:
- Familiar: Most Flutter developers know Provider
- Simple: Minimal boilerplate
- Flexible: Easy to compose multiple providers
- Battle-tested: Mature, stable library
2. Why Separate State Classes?
Alternative: Single monolithic AppState
Why modular state:
- Single responsibility: Each state class has one concern
- Testability: Easier to unit test individual features
- Maintainability: Changes to auth don't affect profile logic
- Team development: Different developers can work on different states
3. Why Upload Queue vs Direct API Calls?
Alternative: Direct API calls from UI actions
Why queue approach:
- Offline capability: Actions work without internet
- User experience: Instant feedback, no waiting for API calls
- Error recovery: Failed uploads can be retried
- Batch processing: Could optimize multiple operations
- Visual feedback: Users can see pending operations
4. Why Overpass + OSM API vs Just One?
Why not just Overpass:
- Overpass doesn't have sandbox data
- Overpass can be unreliable/slow
- OSM API is canonical source
Why not just OSM API:
- OSM API has strict bbox size limits
- OSM API returns all data types (inefficient)
- Overpass is optimized for surveillance device queries
Result: Use the best tool for each situation
5. Why Zoom Level Restrictions?
Alternative: Always fetch, handle errors gracefully
Why restrictions:
- Prevents API abuse: Large bbox queries can overload servers
- User experience: Fetching 10,000 nodes causes UI lag
- Battery life: Excessive network requests drain battery
- Clear feedback: Users understand why nodes aren't showing
Development Guidelines
1. Adding New Features
Before writing code:
- Can we solve this by removing existing code?
- Can we simplify the problem to avoid edge cases?
- Does this fit the existing patterns?
When adding new upload operations:
- Add to
UploadOperationenum - Update
PendingUploadserialization - Add visual state (color, icon)
- Update uploader logic
- Add cache cleanup handling
2. Testing Philosophy
Priority order:
- Integration tests: Test complete user workflows
- Widget tests: Test UI components with mock data
- Unit tests: Test individual state classes
Why integration tests first: The most important thing is that user workflows work end-to-end. Unit tests can pass while the app is broken from a user perspective.
3. Error Handling
Principles:
- Never crash the UI: Always provide fallbacks
- Fail gracefully: Empty list is better than exception
- User feedback: Show meaningful error messages
- Logging: Use debugPrint for troubleshooting
Example pattern:
try {
final result = await riskyOperation();
return result;
} catch (e) {
debugPrint('Operation failed: $e');
// Show user-friendly message
showSnackBar('Unable to load data. Please try again.');
return <EmptyResult>[];
}
4. State Updates
Always notify listeners:
void updateSomething() {
_something = newValue;
notifyListeners(); // Don't forget this!
}
Batch related updates:
void updateMultipleThings() {
_thing1 = value1;
_thing2 = value2;
_thing3 = value3;
notifyListeners(); // Single notification for all changes
}
Build & Development Setup
Prerequisites
- Flutter SDK: Latest stable version
- Xcode: For iOS builds (macOS only)
- Android Studio: For Android builds
- Git: For version control
OAuth2 Setup
Required registrations:
- Production OSM: https://www.openstreetmap.org/oauth2/applications
- Sandbox OSM: https://master.apis.dev.openstreetmap.org/oauth2/applications
Configuration:
cp lib/keys.dart.example lib/keys.dart
# Edit keys.dart with your OAuth2 client IDs
iOS Setup
cd ios && pod install
Running
flutter pub get
flutter run
Testing
# Run all tests
flutter test
# Run with coverage
flutter test --coverage
Code Organization
lib/
├── models/ # Data classes
│ ├── osm_camera_node.dart
│ ├── pending_upload.dart
│ └── node_profile.dart
├── services/ # Business logic
│ ├── map_data_provider.dart
│ ├── uploader.dart
│ └── node_cache.dart
├── state/ # State management
│ ├── app_state.dart
│ ├── auth_state.dart
│ └── upload_queue_state.dart
├── widgets/ # UI components
│ ├── map_view.dart
│ ├── edit_node_sheet.dart
│ └── map/ # Map-specific widgets
├── screens/ # Full screens
│ ├── home_screen.dart
│ └── settings_screen.dart
└── localizations/ # i18n strings
├── en.json
├── de.json
├── es.json
└── fr.json
Principles:
- Models: Pure data, no business logic
- Services: Stateless business logic
- State: Stateful coordination
- Widgets: UI only, delegate to AppState
- Screens: Compose widgets, handle navigation
Debugging Tips
Common Issues
Nodes not appearing:
- Check zoom level (≥10 production, ≥13 sandbox)
- Check upload mode vs expected data source
- Check network connectivity
- Look for console errors
Upload failures:
- Verify OAuth2 credentials
- Check upload mode matches login (production vs sandbox)
- Ensure node has required tags
- Check network connectivity
Cache issues:
- Clear app data to reset cache
- Check if offline mode is affecting behavior
- Verify upload mode switches clear cache
Debug Logging
Enable verbose logging:
debugPrint('[ComponentName] Detailed message: $data');
Key areas to log:
- Network requests and responses
- Cache operations
- State transitions
- User actions
Performance
Monitor:
- Memory usage during large node fetches
- UI responsiveness during background uploads
- Battery usage during GPS tracking
This documentation should be updated as the architecture evolves. When making significant changes, update both the relevant section here and add a brief note explaining the rationale for the change.