Sinew
Concept
Sinews connect muscle to bone. Sinew connects an app's layers: data (API responses, local storage) → domain (plain models, paging rules) → presentation (view state). It's a layered app architecture, shipped as small libraries an app adds one by one, so new apps stop rewriting response mapping, paging, error types, the network client with token refresh, and secure storage.
One sentence: Sinew ships the mechanism; the app ships the configuration.
It pairs with Camouflage:
- Camouflage is the skin (UI); Sinew is what holds the app together underneath.
- The two never depend on each other (Camouflage ADR-27, ADR-29). Only the optional
sinew_camouflageglue knows both. - Sinew targets Flutter and Kotlin (Kotlin Multiplatform, usable from a plain Android app). Both get the same concepts, with names that follow each platform's tools.
The layers
- Envelopes that map themselves. Each envelope names its DTO type and its domain type. The compiler then knows every item has a
toDomain(), so the envelope maps the payload, including every item of a list or page. No repository writes a mapper function. - Errors caught where they happen. Each layer's guard turns failures into a typed
AppExceptionwith amodule-layer-function-typecode (ORD-R-GO-NIC), and hands aResultupward. View models nevertry/catch. - Presentation: page, event, action, effect, state.
- A page sends events.
- The view model maps each event to an action (
mapEventToAction) and handles it (onAction). - It updates one immutable state (
setState) and emits one-off effects (navigation, snackbars) through anEffectEmitter.
- State-driven paging. A
Pager<T>owns the paging rules. A list page only says how to fetch one page.
Packages
v1
| Package | Holds |
|---|---|
sinew_models | Domain, Response, Entity, Envelope and four abstract envelope bases, PagingDomain, ViewState; on Flutter also Result |
sinew_exception | AppException and its codes, handler lists, the guards, CrashReporter |
sinew_paging | Pager<T>, PagingState<T>, LoadType, four paging strategies |
sinew_presentation | the Event → Action → State + Effect base class, EffectEmitter (framework-neutral) |
sinew_riverpod / sinew-viewmodel | framework adapters (mixins and hooks / ViewModel bases) |
sinew_riverpod_providers | ready Riverpod providers for every Sinew service |
sinew_network | processApiCall, the auth layer (single-flight token refresh), timeout and redacting-log layers, network checker, retry and polling |
sinew_storage | SecureStore, KeyValueStore, processStorageCall, the DAO paging helper |
sinew_security | FieldCipher (Tink AEAD), BiometricVault. No hand-rolled cryptography. |
sinew_testing | fakes, mock HTTP setup, test helpers for paging races |
sinew_camouflage | PagingState<T>.toCamo() → CamoPagedListState<T> |
sinew_l10n | English and Indonesian translations for every built-in local error key (Flutter LocalizationsDelegate + ARB, Kotlin Compose resources) |
1.1: DevTools
| Package | Holds |
|---|---|
sinew_devtools | the inspector contract, recorders, redaction, fault injection, host override (no UI) |
sinew_devtools_ui | overlay button, dashboard and built-in inspectors on each platform's Material toolkit |
sinew-devtools-noop (Kotlin) | the same API as empty stubs, for release builds |
Later
Permission, notification, extensions (toMoney, toRelative), devtools inspectors that need native code (EXIF, camera, map), router adapters, a Camouflage skin/theme inspector, and an external devtools companion.
Naming: pub packages are sinew_*. Maven coordinates are com.srctool.sinew:sinew-*, with a sinew-bom. Every package is published on its own and released in lockstep. There's no umbrella package.
Mechanism vs. configuration
| Sinew ships | The app supplies |
|---|---|
| Abstract envelope bases that map payloads | DTOs, and its own envelopes (fields, JSON keys, page metadata) |
| Guards and handler lists | module and function codes |
| The HTTP client factory and its layers | base URLs, one client per backend, SinewConfig |
| The auth layer | TokenSource (where tokens live, how to refresh) |
| Store interfaces and implementations | typed named stores (OrderPreferences) |
The CrashReporter contract (no-op default) | the real reporter |
| Adapters for Riverpod and ViewModel | its own DI wiring |
Doc map
base/ holds the platform-neutral contracts. kotlin/ and flutter/ mirror it by path: kotlin/02 - Packages/03 - Paging implements base/02 - Packages/03 - Paging.
Foundations
- Architecture: layers, the dependency graph, mechanism vs. configuration, publishing.
- Error Model: codes, layers, handler lists, cancellation,
CrashReporter.
Packages
- Models
- Exception
- Paging
- Presentation
- Adapters
- Network
- Security
- Storage
- Testing
- Camouflage Glue
- DevTools
Later (contract sketches)
Project
Platforms
- Kotlin, Flutter.
- Each also has
05 - Delivery/(project setup, publishing, testing and CI), which has no base counterpart. 03 - Later/and04 - Project/have no platform counterparts.
Reading order:
- Architecture, then Error Model.
- The packages in number order. Each depends only on earlier ones.
- DevTools last.
Done when
- A sample app on each platform uses Sinew packages, and one list screen runs through
sinew_camouflageintoCamoPagedList(milestone S5). - 1.0 is published in lockstep on pub.dev and Maven Central (S6). DevTools ships as 1.1 (S7).
Edge cases
| Case | Decided behavior |
|---|---|
| An app uses Sinew without Camouflage | Supported: no Sinew package depends on Camouflage except the optional glue. |
| An app uses Camouflage without Sinew | Supported: CamoPagedList takes a plain state. Any source works through a small adapter. |
| An app wants only paging | Supported: it adds sinew_paging, which brings sinew_models and sinew_exception, and nothing else. |
| The backend wraps payloads differently | Expected: every app declares its own envelopes on Sinew's abstract bases. The guards accept any envelope. |
Implementation
This folder turns the platform-neutral design in Sinew into Kotlin Multiplatform code. It mirrors base/ by path: kotlin/02 - Packages/03 - Paging implements base/02 - Packages/03 - Paging. Every note keeps the base names and meaning, and adds only what Kotlin needs: real signatures, coroutines, expect/actual where a platform really differs, and Gradle.
Toolchain (checked 2026-10-02; re-check at S0)
| Tool | Version | Used for |
|---|---|---|
| Kotlin | 2.4.20 | the language and Gradle plugin (the same toolchain as Camouflage) |
| Android Gradle Plugin | 9.4.1 (Gradle 9.8.1 in the wrapper, JDK 17+) | com.android.kotlin.multiplatform.library for every library module |
| Compose Multiplatform | 1.12.1 (Material 3 is versioned separately: 1.9.0) | only in the modules that need UI or resources: sinew-viewmodel (ObserveEffects), sinew-l10n, sinew-camouflage, sinew-devtools-ui |
| kotlinx.coroutines | 1.11.0 | everywhere |
| kotlinx.serialization | 1.11.0 (stable; 1.12 is in RC) | the network module's JSON |
| Ktor | 3.6.0 | sinew-network |
| AndroidX Lifecycle (multiplatform, JetBrains build) | 2.11.0 | ViewModel in sinew-viewmodel |
| AndroidX DataStore | 1.2.1 | KeyValueStore and the secure store's ciphertext on Android, iOS and JVM |
| Tink | 1.23.0 | FieldCipher and the secure store on Android and the JVM |
| cryptography-kotlin | 0.6.0 | the candidate for AES-GCM on iOS and web (see Security) |
| AndroidX Biometric | 1.1.0 | BiometricVault on Android |
| kotlinx-datetime | 0.8.0 | timestamps in records and retryAfter dates |
| Turbine | 1.2.1 | Flow assertions in tests |
| Kover | 0.9.11 | coverage |
| vanniktech Maven publish | 0.37.0 | publishing through the Central Portal |
Repository and modules
The code lives in sinew-kotlin, the Kotlin submodule of the umbrella repo srctool/sinew.
sinew-kotlin/
├── build-logic/ # the srctool convention plugins (shared with Camouflage)
├── gradle/*.versions.toml # split version catalogs
├── sinew-models/ sinew-exception/ sinew-l10n/
├── sinew-paging/ sinew-presentation/ sinew-viewmodel/
├── sinew-network/ sinew-security/ sinew-storage/
├── sinew-testing/ sinew-camouflage/
├── sinew-devtools/ sinew-devtools-ui/ sinew-devtools-noop/
├── sinew-bom/
└── sample/
├── shared/ # KMP: the sample list screen (commonMain) + desktop and web entry points
├── androidApp/
└── iosApp/ # Xcode project, a thin entry point
Package root: com.srctool.sinew.<module> (com.srctool.sinew.paging, com.srctool.sinew.network). Maven coordinates: com.srctool.sinew:sinew-<module>.
Base → Kotlin map
| Base | Kotlin |
|---|---|
| Architecture | Architecture (api vs implementation, Koin wiring) |
| Error Model | Error Model (kotlin.Result, cancellation) |
| Models | Models |
| Exception | Exception (+ sinew-l10n) |
| Paging | Paging |
| Presentation | Presentation |
| Adapters | Adapters (sinew-viewmodel) |
| Network | Network |
| Security | Security |
| Storage | Storage |
| Testing | Testing |
| Camouflage Glue | Camouflage Glue |
| DevTools | DevTools |
| (none) | Project Setup, Publishing, Testing and CI |
base/03 - Later and base/04 - Project have no Kotlin notes. 05 - Delivery has no base note.
Kotlin-wide conventions for Sinew
explicitApi()in every library module: every public declaration sayspublic, so the API surface is deliberate and the ABI dump readable.commonMainfirst.expect/actualonly where a platform really differs: the HTTP engine, secure storage, crypto keys, biometrics, the network checker, uncaught-error hooks.- Suspend functions, not callbacks. Repository-facing APIs are
suspend. Streams areFlow. A long-running operation runs in the caller's scope. kotlin.Resulteverywhere, built only by Sinew's guards.runCatchingis never used around suspend calls, because it swallowsCancellationException.- Hand-written sealed types, no code generation.
ViewState,PagingState,ErrorMessageand the exception types aresealedclasses or interfaces. - No DI in libraries. Constructors and factories only. Koin wiring is an app concern, shown in the Architecture note.
- Compose only where it's needed. The four modules listed in the toolchain table. Everything else is plain Kotlin and works in a non-Compose app.