Flows
The pages in this section follow one user action at a time through every layer it touches: the widget that receives the tap, the Riverpod notifier that owns the state, the repository or channel that talks to the network, and the Phoenix module that answers. Each one has a sequence diagram, then a walkthrough with a file and line reference at each hop.
This page is the map. Every box is a screen, labelled with its go_router path from
app/lib/app/routes.dart:2-21. The routes themselves are declared in
app/lib/app/router.dart:41-122.
The journey
Section titled “The journey”flowchart TD
start([App launch]) --> splash["/splash<br/>SplashScreen"]
splash -->|no token, or 401 on /api/me| login["/login<br/>LoginScreen"]
splash -->|token accepted, or offline with cached user| home
login <-->|push / pop| register["/register<br/>RegisterScreen"]
login -->|sign in| home
register -->|create account| home
subgraph shell [HomeShell tabs]
home["/home<br/>RaceTab"]
activity["/activity<br/>ActivityTab"]
history["/history<br/>HistoryTab"]
profile["/profile<br/>ProfileTab"]
end
home -->|stored active race still on| race
home -->|tap distance, confirm,<br/>PermissionsSheet| queue["/queue/:raceType<br/>QueueScreen"]
home -->|Browse lobby| lobby["/lobby<br/>LobbyScreen"]
queue -->|match, under 5 s to start| race["/race/:id<br/>RaceScreen"]
queue -->|Cancel| home
lobby -->|join a waiting runner| race
race -->|finished or left| result["/race/:id/result<br/>ResultScreen"]
result -->|Done| home
result -->|Race again| queue
history -->|row| result
profile -->|Sign out| login
shell -.->|any 401 from the API| login
Two things the diagram does not show. The splash, login and register screens are never
navigated to by a button: the router’s redirect puts you there whenever the auth status
says so (app/lib/app/router.dart:136-147). And the race and queue screens sit outside the
tab shell, so going to them replaces the tabs entirely. That is why the result screen’s
“Done” is a context.go(AppRoutes.home) (app/lib/features/race/result_screen.dart:177-181)
rather than a pop.
The flows
Section titled “The flows”| Page | Starts at | Ends at | Main owners |
|---|---|---|---|
| Auth | cold start | Race tab or sign-in | AuthNotifier, TokenStore, UserCache, router redirect, Dio interceptor |
| Matchmaking | tap a distance | race screen countdown | RaceTab, PermissionsSheet, QueueNotifier, WaitingChannel, LobbyScreen |
| Race | countdown | result screen | RaceNotifier, RaceChannel, ForegroundRaceTracker, the service isolate, PositionUploader |
| Failure modes | something goes wrong | wherever the code leaves you | all of the above |
Which code runs where
Section titled “Which code runs where”Most of the app runs in one Dart isolate, the main (UI) isolate, where every widget and
every provider lives. During a race there is a second one: the foreground-service isolate
started by flutter_foreground_task, which samples GPS and uploads positions on its own.
Think of the two as BEAM processes with separate heaps: they share no memory and talk only
by message. The race page spends a lot of time on that boundary, because it decides what
survives a locked screen and what does not. For the mechanics, read
Tracking and Realtime.
On the backend, the flows touch three areas you already know: the JSON controllers under
backend/lib/footrace_web/controllers/api/, the two channels WaitingChannel and
RaceChannel in backend/lib/footrace_web/channels/, and the per-race GenServer
Racing.RaceProcess in backend/lib/racing/race_process.ex.
How to read the references
Section titled “How to read the references”A reference such as app/lib/features/race/race_notifier.dart:300-317 is a range in the
Flutter repo at main @ d1b602a. A backend/... reference is the local Phoenix checkout
at d7b9c91. That checkout was read as it was on disk, not freshly fetched, so a backend
line number may have moved on the remote. Where the app’s behaviour and the use-case docs in
footrace-flutter/docs/use-cases/ disagree, the pages say so and side with the code.