docs: record the layering rule and where it is enforced

The architecture section listed files but said nothing about which layer may
import what, so the direction of dependency was only visible by reading the
imports. States the rule as a table, notes that the UI depends on the
Iperf3Runner contract rather than on a transport and what that buys, and points
at the test that enforces it so the rule is not just prose.

Also brings the tree up to date: it was missing the domain services directory,
the event model and the formatter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 19:56:25 +09:00
parent 0cf019379a
commit fe042efc29
+34 -8
View File
@@ -6,19 +6,23 @@ A Flutter-based Android GUI wrapper for iperf3, the popular network performance
``` ```
lib/ # Flutter/Dart application layer lib/ # Flutter/Dart application layer
├── main.dart # App entry point (MaterialApp) ├── main.dart # Composition root: picks the Iperf3Runner impl
├── domain/ # Business logic (platform-independent) ├── domain/ # Pure Dart — no Flutter import
│ ├── models/ │ ├── models/
│ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams │ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams
│ │ └── iperf3_result.dart # Raw output + exit code │ │ ├── iperf3_result.dart # Raw output, exit code, stopped flag
├── data/ │ │ └── iperf3_event.dart # Parsed --json-stream reports
│ └── services/ │ └── services/
│ └── iperf3_service.dart # Dart → MethodChannel bridge (async with Completer) │ └── iperf3_runner.dart # The contract the UI depends on
└── ui/ # Presentation layer ├── data/ # Transport — flutter/services only
│ └── services/
│ └── iperf3_service.dart # Iperf3Runner over MethodChannel → JNI
└── ui/ # Presentation
└── features/ └── features/
└── test_run/ └── test_run/
├── test_run_screen.dart # Server/port inputs + GO button ├── test_run_screen.dart # Inputs, GO/STOP, live transcript
└── test_run_view_model.dart # State machine: idle → running → completed/error ├── test_run_view_model.dart # idle → running → completed/error
└── iperf3_event_formatter.dart # Events → iperf3-style text
android/app/src/main/ android/app/src/main/
├── kotlin/ # Kotlin plugin layer ├── kotlin/ # Kotlin plugin layer
@@ -48,6 +52,28 @@ User taps GO
→ On completion: Dart Completer resolves with Iperf3Result → On completion: Dart Completer resolves with Iperf3Result
``` ```
## Layering
Dependencies point inward: `ui → domain ← data`, never the reverse.
| Layer | May import | Holds |
|---|---|---|
| `domain` | nothing else in this package, no Flutter | models and the `Iperf3Runner` contract |
| `data` | `domain`, `package:flutter/services.dart` | the MethodChannel/JNI implementation |
| `ui` | `domain` | widgets, view model, formatting |
| `main.dart` | everything | the single place that picks an implementation |
The UI depends on `Iperf3Runner`, not on `Iperf3Service`, so presentation names
no transport. That is what lets `test/widget_test.dart` drive the real widget
tree against an in-memory runner with no MethodChannel, native library or
device — and what keeps a test double at five stable members instead of
mirroring the transport's whole surface.
`test/architecture_test.dart` enforces all of this by reading the actual
imports under `lib/`, including a check that no fourth layer directory has
appeared unnoticed. Change the layering there when the layering should change;
don't work around it.
## Key Design Decisions ## Key Design Decisions
### JNI Over Process Execution ### JNI Over Process Execution