Skip to content

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.

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.

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

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.

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.