Owns navigation, and the one process per live screen that serves it.
Which stacks exist, which is active, and one Mob.Screen.Server per live
screen — this process starts them, stops them, and restarts one that crashes.
It keeps the :mob_screen registered name, so the native layer's
enif_whereis_pid lookups (back gesture, alert actions, launch
notifications) are unaffected.
Not in the per-message path
A screen handling an ordinary message never touches this process. Native
events reach a screen directly: Mob.Listener unwraps the envelope and sends
to the screen's own pid, the screen renders, and Mob.Sender commits. The
router hears only about navigation.
That is the property MOB-113 exists to guarantee, and it is what makes one process per screen affordable. An earlier costing of this design assumed a router in the loop and concluded per-screen processes could not escape a hop per message; splitting the router from the sender is what dissolved that.
Mob.Screen delegates its public API here, so callers keep using
Mob.Screen.dispatch/3 and friends.
A navigation entry
%{module:, pid:, params:, ref:}.
params is carried because a restart has to reproduce the screen exactly —
one that mounts on %{id: id} cannot come back from %{}.
ref identifies the screen to Mob.Sender, and is unique per screen,
not per stack. Every screen is a live process that repaints on any message it
receives, including the ones below the top of a stack; keyed by stack, a timer
tick in a screen the user cannot see would commit its tree — tap table
included — over the screen they can. The sender only commits the tree whose
ref is active. The ref survives a restart, because the replacement is the same
logical screen.
See decisions/2026-08-28-screen-processes-and-supervision.md.
Summary
Functions
Returns a specification to start this module under a supervisor.
Dispatch a UI event to the screen process. Returns :ok synchronously once
the event has been processed and the state updated.
Every screen entry the router holds, as {module, pid} — current, history and
parked tabs alike.
Return the module of the currently active screen in the navigation stack. Intended for testing and debugging.
Return the navigation history (list of {module, socket} pairs, head = most recent).
Intended for testing and debugging.
Return the pid of the process owning the currently active screen.
Return the current socket state of a running screen, or nil while that
screen is being restarted.
Start a screen process linked to the calling process.
Start a screen as the root UI screen. Calls mount, renders the component tree
via Mob.Renderer, and calls set_root on the resulting view.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
Dispatch a UI event to the screen process. Returns :ok synchronously once
the event has been processed and the state updated.
Every screen entry the router holds, as {module, pid} — current, history and
parked tabs alike.
For Mob.Invariant: get_nav_history/1 returns sockets, which is what a test
wants, but an invariant needs the pids in order to ask whether they are still
alive.
Return the module of the currently active screen in the navigation stack. Intended for testing and debugging.
@spec get_screen_pid(GenServer.server()) :: pid()
Return the pid of the process owning the currently active screen.
Each live screen is its own process since MOB-112; this is how tooling reaches the one that is on screen.
@spec get_socket(pid()) :: Mob.Socket.t() | nil
Return the current socket state of a running screen, or nil while that
screen is being restarted.
Intended for testing and debugging — not for production app logic.
@spec start_link(module(), map(), keyword()) :: GenServer.on_start()
Start a screen process linked to the calling process.
params is passed as the first argument to mount/3.
@spec start_root(module(), map(), keyword()) :: GenServer.on_start()
Start a screen as the root UI screen. Calls mount, renders the component tree
via Mob.Renderer, and calls set_root on the resulting view.
This is the main entry point for production use. start_link/2 is for tests
(no NIF calls).
When no screen is left
If the root screen fails to start (its mount/3 returns an error or
raises) while no other router is live, or the current screen crashes,
can't be restarted and there is no other live screen to fall back to, the
app would sit on a blank screen for good: the process stays alive, so
relaunching from the launcher brings the same blank process back. On a
device (the real :mob_nif), the router logs the reason, flushes the
logger and ends the process with System.halt(1), so the next launch boots
fresh. That also lets a plugin that watches for a launch that never
becomes stable (mob_deliver's probation) see this one die.
:on_no_live_screen replaces that behaviour with a 1-arity function called
with the module of the screen that failed. Anything injected as :nif
(host tests) gets no action by default, so a test VM is never halted.