Files
vnidrop/.codex/skills/compose-skill/references/architecture.md
Hammed Abass 6ab658fea2 feat(shared): graduate saved-device transfer experience
Add the VniDrop-specific Compose architecture skill, unify invitation and targeted transfer drafts, and promote Saved devices to an adaptive first-class destination.
2026-08-12 21:45:18 +02:00

101 lines
3.6 KiB
Markdown

# VniDrop presentation architecture
Use for feature structure, state ownership, deepening, or ViewModel/UI seams.
## Module shape
```text
Route
├─ collects StateFlow
├─ invokes platform adapters
├─ collects semantic effects
└─ navigates
Deep presentation module
├─ immutable state
├─ named methods
├─ domain orchestration
└─ internal seams
CoreGateway + platform adapters
```
| Concern | Owner |
|---|---|
| Domain-facing presentation state | Feature ViewModel/module |
| Platform picker invocation | Route |
| Navigation | Route/app navigation |
| Transfer creation and durable state | Rust through `CoreGateway` |
| Source preparation | Android/JVM adapter |
| Rendering | Screen and leaf composables |
| One-shot feedback | Existing effect/`UiMessageController` pattern |
## Depth checks
- Apply the deletion test: deleting a deep module must spread meaningful behavior back across multiple callers.
- The interface is the test surface. If tests need private state, reshape the module.
- Keep adapters internal unless callers genuinely select them.
- Prefer a concrete class when only one implementation exists.
- Do not create an `Actions` bag to conceal a wide interface.
- Do not split by arbitrary file size when the pieces still share one interface and invariant set.
## State rules
- Make states immutable and equality-friendly.
- Store durable/product-significant state in the module; keep focus, scroll, animation, and transient expansion local to Compose.
- Derive values instead of storing duplicates.
- Track provenance when derivation must stop after user editing, such as automatic Transfer draft names.
- Model loading without discarding valid content.
- Freeze invalid concurrent operations explicitly in state.
## Transfer draft seam
The external seam is a session-scoped MVVM module used by both callers.
```kotlin
class TransferDraftViewModel(...) : ViewModel() {
val state: StateFlow<TransferDraftState>
val outputs: Flow<TransferDraftOutput>
fun openInvitation(defaultSenderName: String)
fun openTargeted(device: LockedSavedDevice)
fun chooseFiles()
fun chooseFolder()
fun onPickerResult(requestId: Long, result: Result<List<PickedShareFile>>)
fun changeTransferName(value: String)
fun removeSource(id: DraftSourceId)
fun submit()
fun dismiss()
}
```
The exact implementation may evolve, but preserve these invariants:
- One immutable destination per open session.
- One in-flight picker and submission.
- Correlate picker callbacks; discard stale owned copies.
- Atomic source replacement.
- Failure preserves the draft.
- Success/dismissal releases owned copies exactly once.
- Semantic creation output; no navigation inside the module.
- Invitation and Targeted results remain distinct.
## Migration order
1. Extract Invitation composition and interface-level tests.
2. Switch Targeted creation to the same module.
3. Delete the one-shot Saved-device picker flow.
4. Promote Saved devices into its own route.
5. Add Targeted transfer detail and lifecycle presentation.
6. Remove the experimental UI gate atomically after parity.
## Anti-patterns
| Anti-pattern | Better replacement |
|---|---|
| Parent ViewModels duplicate source rules | One Transfer draft module |
| `FileSystemService` creates transfers | Source adapter prepares; module calls `CoreGateway` |
| Generic transfer type erases domain | Explicit Invitation/Targeted variants |
| Route contains state machine | Route binds adapters and navigation only |
| Tests mock internals | Test through module state/methods/outputs |