Thanks for your interest in contributing. Sheaf handles sensitive identity data, so security and privacy aren't optional — please keep that in mind across changes.
- Android Studio Ladybug (2024.2.1) or newer
- JDK 17 (bundled with Android Studio)
- Android SDK platform 35
The app points to a user-configured base URL (set in Settings → API Server). For development, run the Sheaf API locally (uvicorn sheaf.main:app --reload) and point the base URL at your machine (e.g. http://10.0.2.2:8000 for an emulator).
Run ./gradlew lint before submitting a PR.
- Language: Kotlin, Jetpack Compose (Material 3)
- DI: Hilt (KSP)
- Networking: Retrofit 2 + OkHttp 3, Moshi (KSP codegen)
- Navigation: Jetpack Navigation Compose
- Storage: DataStore Preferences (via
PreferencesRepository) - Images: Coil
- Async: Kotlin Coroutines + StateFlow
- Wear OS: Wear Compose Material, Wear Tiles, Wearable Data Layer
- ViewModels use a single
UiStatedata class withisLoading,error: String?, and operation-specific flags (isSaving,isDeleting, etc.). State is aMutableStateFlowupdated via.update { it.copy(...) }. runCatching+.onSuccess/.onFailureis the standard pattern for all API calls. Never throw from a ViewModel.- HTTP errors: catch
HttpExceptionand check.code()to produce user-friendly messages. Don't surface raw status codes. - Navigation signals: boolean flags on state (
saved,deleted) observed withLaunchedEffectto trigger navigation after an operation completes. - Form state lives in a separate
MutableStateFlowfrom UI state, updated via_form.update { copy(...) }. - IDs are UUIDs (strings in Kotlin, matching the API).
- Moshi codegen: request/response models need
@JsonClass(generateAdapter = true). Snake_case JSON fields use@Json(name = "..."). - Base URL is dynamic — users configure it in settings. Never hardcode URLs. Retrofit is initialised with
http://localhost/andBaseUrlInterceptorrewrites it. - American spelling throughout — "color" not "colour", matching the iOS codebase.
- Material Design 3 — use theme tokens (
MaterialTheme.colorScheme.*,MaterialTheme.shapes.*), never hardcode colors or corner radii.
Unit tests live in sheaf/app/src/test/ and cover ViewModel logic.
Stack: JUnit 4, MockK, kotlinx-coroutines-test, Turbine (StateFlow assertions).
Pattern:
MainDispatcherRulereplacesDispatchers.MainwithUnconfinedTestDispatcherso coroutines run synchronously.SheafApiServiceandPreferencesRepositoryare mocked with MockK.FrontNotificationHelperis alwaysmockk(relaxed = true)(Android dependency, side-effect only).- ViewModels that call
load()ininitmust have API stubs configured before constructing the ViewModel. - Use
coEvery/coVerifyfor suspend functions;every/verifyfor regular functions and Flow properties.
- User logs in → API returns access + refresh tokens.
AuthViewModelcheckstotpEnabledon the returned user.- If TOTP is required, tokens are held in
pendingAccessToken/RefreshTokenuntil the code is verified, then persisted. TokenAuthenticatorhandles 401s automatically: refreshes and retries transparently.- Logout clears both tokens from DataStore.
Credentials are pushed phone → watch via the Wearable Data Layer:
- Phone:
PhoneDataLayerServicelistens for/sheaf/credentials/requestmessages, responds by writing aDataItemat/sheaf/credentialswithbase_url,access_token,refresh_token. - Watch:
WearDataLayerServicewatches for thatDataItemand saves credentials viaWearAuthManager. - On first launch,
MainActivityon the watch sends a request message to the phone if not yet authenticated.
AGPL-3.0-or-later.