Skip to main content

Low Level Design

Validation Development

Basic Plot Validations

XLSForm

FieldValueDescription
typetext
namevalidate_polygon
requiredtrue
appearanceex:com.akvo.externalodk.VALIDATE_POLYGON(shape=${target_field})
constraint. = ${manual_boundary}
constraint_messagePlease revalidate by clicking "Launch" button

Full source: Spreadsheet - African Bamboo B.V. Parcel Boundary Mapping

 

Geometry Validation Logic and Response

 

ODK External Validation ScreenDescription
Basic Plot validation_Error_validation_is_required.jpegThis response 
WhatsApp Image 2026-01-28 at 3.38.24 PM (1).jpeg
Basic Plot validation_Error_Shape_too_small.jpeg
Basic Plot validation_Error_Shape_line_intersect.jpeg
Basic Plot validation_Error_Previous validation value is not match with current value.jpeg
Basic Plot validaton_Success.jpeg

 

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 next URL 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 REPLACE
  • getSubmissions(assetUid): Flow<List> - Reactive list
  • getByUuid(uuid) - Single submission lookup
  • getCount(assetUid) - Total count
  • getLatestSubmissionTime(assetUid) - For display
  • deleteByAssetUid(), deleteAll() - Cleanup

FormMetadataDao (data/dao/FormMetadataDao.kt)

  • insertOrUpdate() - Upsert pattern
  • getLastSyncTimestamp(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:

  1. Validate all fields non-empty
  2. Save credentials to EncryptedSharedPreferences
  3. 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:

  1. Construct URL: {server_url}/api/v2/assets/{form_id}/data.json
  2. Fetch all pages (limit=10, follow next)
  3. Transform JSON → SubmissionEntity list
  4. Batch insert to Room
  5. Store lastSyncTimestamp
  6. 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:

  1. Get lastSyncTimestamp from FormMetadataDao
  2. API call with query parameter (dates >= timestamp)
  3. Diff against local:
    • Added: _uuid not in Room → Insert
    • Updated: _uuid exists AND remote submissionTime > local → Update
  4. Count added/updated records
  5. Update lastSyncTimestamp to now
  6. 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_case keys to Title Case labels
  • Handles all JSON types (primitives, arrays, objects)

5. Navigation

File: navigation/Routes.kt, navigation/AppNavHost.kt

image.png

Route Definitions:

  • Login - Entry point for logged-out users
  • Loading(type: LoadingType) - DOWNLOAD or RESYNC
  • DownloadComplete(totalEntries, latestSubmissionDate)
  • Home - Entry point for logged-in users
  • SubmissionDetail(uuid)
  • SyncComplete(addedRecords, updatedRecords, latestRecordTimestamp)

6. Architecture

Pattern: MVVM + Clean Architecture

image.png

Dependency Injection

Framework: Hilt

Module Provides
NetworkModule KoboApiService, OkHttpClient, Json
DatabaseModule AppDatabase, DAOs

State Management

  • ViewModels: StateFlow for UI state
  • Database: Flow<List<T>> for reactive lists
  • Collection: collectAsStateWithLifecycle() in Composables

7. Security

Credential Storage

File: data/session/SessionManager.kt

  • EncryptedSharedPreferences with AES256_GCM
  • MasterKey with AES256_GCM scheme
  • Stores: username, password, serverUrl, assetUid

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

  1. Destructive migrations: fallbackToDestructiveMigration() will lose data on schema changes
  2. Test coverage: Minimal unit/instrumented tests
  3. Accessibility: Limited contentDescription coverage

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": "..."
    }
  ]
}