Low Level Design
Validation Development
Basic Plot Validations
XLSForm
| Field | Value | Description |
| type | text | |
| name | validate_polygon | |
| required | true | |
| appearance | ex:com.akvo.externalodk.VALIDATE_POLYGON(shape=${target_field}) | |
| constraint | . = ${manual_boundary} | |
| constraint_message | Please revalidate by clicking "Launch" button |
Full source: Spreadsheet - African Bamboo B.V. Parcel Boundary Mapping
Geometry Validation Logic and Response
| ODK External Validation Screen | Description |
![]() |
This response |
![]() |
|
![]() |
|
![]() |
|
![]() |
|
![]() |
Application Development
1. Overview
The African Bamboo - ODK External Validations is an Android client for KoboToolbox API integration. It downloads form submissions, stores them locally, and supports incremental sync for offline access.
Implementation Progress
| Component | Status | Notes |
|---|---|---|
| Authentication | ✅ Complete | Encrypted storage with BasicAuth |
| API Integration | ✅ Complete | Pagination, delta sync |
| Database | ✅ Complete | Room with hybrid schema |
| All 6 Screens | ✅ Complete | Jetpack Compose + Material 3 |
| Navigation | ✅ Complete | Type-safe routes |
| ViewModels | ✅ Complete | StateFlow/MVVM |
| Geoshape Display | ⏳ Planned | Map thumbnails not yet implemented |
| Testing | 🔴 Minimal | Test skeletons only |
2. API Integration
Endpoint
GET /api/v2/assets/{asset_uid}/data.json
Authentication
Implementation: BasicAuthInterceptor.kt
- Uses HTTP Basic Auth via OkHttp Interceptor
- Credentials stored in
EncryptedSharedPreferences - Dynamic base URL support for custom Kobo instances
Pagination
Implementation: KoboRepository.kt
- Page size:
limit=10 - Cursor-based: follows
nextURL until null - All pages inserted within single transaction
Delta Sync (Resync)
Query Parameter:
query={"_submission_time": {"$gte": "{last_sync_timestamp}"}}
- Timestamp format: ISO 8601 (UTC)
- Stored in
FormMetadataEntity.lastSyncTimestamp
3. Data Model & Database Schema
Architecture Decision: Hybrid Storage
Instead of form-specific columns, we use:
- Typed columns for system fields (
_uuid,submissionTime,submittedBy) - JSON blob (
rawData) for all form answers
This enables support for any KoboToolbox form without schema changes.
Entity: SubmissionEntity
File: data/entity/SubmissionEntity.kt
| Field | Type | Source | Description |
|---|---|---|---|
_uuid |
String (PK) | _uuid |
Unique submission identifier |
assetUid |
String | Form ID | Links submission to form |
_id |
Int | _id |
Kobo's internal ID |
submissionTime |
Long | _submission_time |
Parsed ISO 8601 → timestamp |
submittedBy |
String? | _submitted_by |
Username of submitter |
instanceName |
String? | meta/instanceName |
Form instance name |
rawData |
String | Full JSON | Complete submission payload |
systemData |
String? | Extracted | Geolocation, tags, etc. |
Indices: assetUid, submissionTime, instanceName, _uuid
Entity: FormMetadataEntity
File: data/entity/FormMetadataEntity.kt
| Field | Type | Description |
|---|---|---|
assetUid |
String (PK) | Form identifier |
lastSyncTimestamp |
Long | Last successful sync time |
DAOs
SubmissionDao (data/dao/SubmissionDao.kt)
insertAll(),insert()- Bulk/single insert with REPLACEgetSubmissions(assetUid): Flow<List>- Reactive listgetByUuid(uuid)- Single submission lookupgetCount(assetUid)- Total countgetLatestSubmissionTime(assetUid)- For displaydeleteByAssetUid(),deleteAll()- Cleanup
FormMetadataDao (data/dao/FormMetadataDao.kt)
insertOrUpdate()- Upsert patterngetLastSyncTimestamp(assetUid)- For delta sync
4. Screen Specifications
Screen 1: Login
File: ui/screen/LoginScreen.kt ViewModel: LoginViewModel.kt
| Element | Implementation |
|---|---|
| Username field | Text input, required |
| Password field | Password input, obscured |
| Server URL | Text, default: https://eu.kobotoolbox.org |
| Form ID | Text, required |
| Submit button | "Download Data" |
Workflow:
- Validate all fields non-empty
- Save credentials to
EncryptedSharedPreferences - Navigate to Loading screen (DOWNLOAD type)
Note: Original spec called for /token endpoint. Implementation uses BasicAuth directly—no token exchange needed.
Screen 2: Download Loading
File: ui/screen/LoadingScreen.kt ViewModel: LoadingViewModel.kt
| Element | Implementation |
|---|---|
| Spinner | CircularProgressIndicator |
| Message | "Downloading data..." |
| Error state | Retry + Back to Login buttons |
Logic:
- Construct URL:
{server_url}/api/v2/assets/{form_id}/data.json - Fetch all pages (limit=10, follow
next) - Transform JSON →
SubmissionEntitylist - Batch insert to Room
- Store
lastSyncTimestamp - Navigate to Download Complete with metrics
Screen 3: Download Complete
File: ui/screen/DownloadCompleteScreen.kt
| Element | Implementation |
|---|---|
| Total entries | Passed via navigation args |
| Latest submission | Formatted date string |
| "View Data" button | Navigate to Home |
| "Resync Data" button | Navigate to Loading (RESYNC) |
Screen 4: Home/Dashboard
File: ui/screen/HomeDashboardScreen.kt ViewModel: HomeViewModel.kt
| Element | Implementation |
|---|---|
| Submission list | LazyColumn with Flow<List> |
| Search | TopAppBar search field |
| Sort | Bottom sheet (Name A-Z/Z-A, Date New/Old) |
| Resync FAB | Navigate to Loading (RESYNC) |
| Logout | Menu option with confirmation |
| Item click | Navigate to Submission Detail |
Geoshape Indicator: ⏳ Not yet implemented. LLD specified map thumbnails or "Map Available" badge.
Screen 5: Resync Loading
File: ui/screen/LoadingScreen.kt (shared with Download)
| Element | Implementation |
|---|---|
| Spinner | CircularProgressIndicator |
| Message | "Syncing data..." |
Delta Sync Logic:
- Get
lastSyncTimestampfromFormMetadataDao - API call with
queryparameter (dates >= timestamp) - Diff against local:
- Added:
_uuidnot in Room → Insert - Updated:
_uuidexists AND remotesubmissionTime> local → Update
- Added:
- Count added/updated records
- Update
lastSyncTimestampto now - Navigate to Sync Complete with counts
Screen 6: Sync Complete
File: ui/screen/SyncCompleteScreen.kt
| Element | Implementation |
|---|---|
| Added records | Count from sync |
| Updated records | Count from sync |
| Latest sync time | Current timestamp |
| "Return to Dashboard" | Navigate to Home |
Screen 7: Submission Detail
File: ui/screen/SubmissionDetailScreen.kt ViewModel: SubmissionDetailViewModel.kt
| Element | Implementation |
|---|---|
| Title | Instance name or formatted date |
| Submitted on | Formatted timestamp |
| Submitted by | Username |
| Answers list | Parsed from rawData JSON |
Answer Parsing:
- Filters out system fields (prefix
_,meta,formhub) - Converts
snake_casekeys toTitle Caselabels - Handles all JSON types (primitives, arrays, objects)
5. Navigation
Route Definitions:
Login- Entry point for logged-out usersLoading(type: LoadingType)- DOWNLOAD or RESYNCDownloadComplete(totalEntries, latestSubmissionDate)Home- Entry point for logged-in usersSubmissionDetail(uuid)SyncComplete(addedRecords, updatedRecords, latestRecordTimestamp)
6. Architecture
Pattern: MVVM + Clean Architecture
Dependency Injection
Framework: Hilt
| Module | Provides |
|---|---|
NetworkModule |
KoboApiService, OkHttpClient, Json |
DatabaseModule |
AppDatabase, DAOs |
State Management
- ViewModels:
StateFlowfor UI state - Database:
Flow<List<T>>for reactive lists - Collection:
collectAsStateWithLifecycle()in Composables
7. Security
Credential Storage
File: data/session/SessionManager.kt
Network Security
- BasicAuth over HTTPS only
- No token stored (credentials used per-request)
- Dynamic base URL validated before use
8. Definition of Done
| Requirement | Status | Evidence |
|---|---|---|
| MVVM with StateFlow | ✅ | All 4 ViewModels use MutableStateFlow |
| Jetpack Compose UI | ✅ | All screens in ui/screen/ |
collectAsStateWithLifecycle |
✅ | Used in all screen Composables |
| Login + encrypted session | ✅ | SessionManager with AES256 |
| Pagination (no missing records) | ✅ | KoboRepository.fetchSubmissions() follows next |
| Delta sync (Added vs Updated) | ✅ | KoboRepository.resync() with diffing |
| Offline access (Room cache) | ✅ | SubmissionDao.getSubmissions() returns cached data |
| Password/token encrypted | ✅ | EncryptedSharedPreferences |
9. Known Gaps & Future Work
Not Yet Implemented
| Feature | Priority | Notes |
|---|---|---|
| Geoshape map display | Medium | Show map thumbnail in list items |
| Geolocation validation | Low | External validation logic |
| Database migrations | High | Currently uses destructive migration |
| Comprehensive tests | High | Only skeleton files exist |
| Offline-first indicators | Low | No explicit offline mode UI |
| Error logging/Crashlytics | Medium | No crash reporting |
Technical Debt
- Destructive migrations:
fallbackToDestructiveMigration()will lose data on schema changes - Test coverage: Minimal unit/instrumented tests
- Accessibility: Limited
contentDescriptioncoverage
10. File Structure
app/src/main/java/com/akvo/externalodk/
├── data/
│ ├── dao/ # SubmissionDao, FormMetadataDao
│ ├── database/ # AppDatabase
│ ├── dto/ # KoboSubmissionDto, KoboDataResponse
│ ├── entity/ # SubmissionEntity, FormMetadataEntity
│ ├── network/ # KoboApiService, Interceptors
│ ├── repository/ # KoboRepository
│ └── session/ # SessionManager, AuthCredentials
├── di/ # Hilt modules
├── navigation/ # Routes, AppNavHost
└── ui/
├── screen/ # All 6 Composable screens
├── theme/ # Material 3 theming
└── viewmodel/ # Login, Loading, Home, SubmissionDetail
Appendix: API Response Example
{
"count": 1250,
"next": "https://kf.kobotoolbox.org/api/v2/assets/xxx/data.json?start=300",
"previous": null,
"results": [
{
"_id": 12345,
"_uuid": "abc-123-def",
"_submission_time": "2026-01-15T10:30:00Z",
"_submitted_by": "field_worker",
"meta/instanceName": "Survey 001",
"question_1": "Answer 1",
"question_2": 42,
"geoshape": "..."
}
]
}







