Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ Two distinct layers, each handling a different class of failure:

Most "crashes" in a Mob app are BEAM process exits with a structured reason and stacktrace — OTP gives you this for free. These can be captured without any native SDK:

- `Mob.Screen.terminate/2` is called on every screen process exit — hook in here to capture the reason + stacktrace
- `Mob.Screen.terminate/2` is called when the single `Mob.Screen` GenServer (which owns the whole navigation stack) exits — hook in here to capture the reason + stacktrace
- OTP `Logger` already receives supervision tree crash reports as `:error` level messages — `Mob.NativeLogger` captures these natively, a crash reporter can also forward them
- A `Mob.CrashReporter` module (separate opt-in package) would collect these, batch them, and POST to a reporting backend over HTTP using `req` or `finch`

Expand Down
2 changes: 1 addition & 1 deletion guides/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ The framework automatically picks the right animation based on the navigation ac

## Passing data on pop

Mob's navigation is process-based. When you pop back to a previous screen, that screen's process is still running with its original state. To pass data back, send a message to the parent's pid.
When you push a screen, Mob snapshots its `{module, socket}` into the stack history. Popping back restores that snapshot — it runs no `mount/3` again and all screens in a stack share the same `Mob.Screen` GenServer. To pass data back, send a message to the parent's pid.

Pass the parent pid as a param when pushing:

Expand Down
10 changes: 5 additions & 5 deletions guides/screen_lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Screen Lifecycle

A Mob screen is a GenServer wrapped by `Mob.Screen`. Each screen in the navigation stack is a separate, supervised process. Understanding the lifecycle means understanding when each callback fires and what you can do in it.
A Mob screen is a GenServer wrapped by `Mob.Screen`. One `Mob.Screen` GenServer owns the whole navigation stack: the active screen is a `{module, socket}` pair in the process state, and `nav_history` keeps a snapshot of each pushed screen. A push mounts the next module in that same process; a pop restores the previous snapshot — it neither starts a new process nor calls `terminate/2`. Understanding the lifecycle means understanding when each callback fires and what you can do in it.

## Callbacks

Expand All @@ -11,7 +11,7 @@ A Mob screen is a GenServer wrapped by `Mob.Screen`. Each screen in the navigati
{:ok, Mob.Socket.t()} | {:error, term()}
```

Called once when the screen process starts. Initialize your assigns here.
Called when the screen first enters the navigation stack — via `start_root/2` or when it is pushed. The mounted socket is snapshotted into `nav_history`; popping back to this screen restores that snapshot and does **not** call `mount/3` again.

`params` comes from the navigation call that opened this screen:

Expand Down Expand Up @@ -142,9 +142,9 @@ The default implementation (from `use Mob.Screen`) raises for any unhandled even
@callback terminate(reason :: term(), socket :: Mob.Socket.t()) :: term()
```

Called when the screen process is about to stop. Use it for cleanup — cancel timers, release resources. The return value is ignored.
Called only when the whole `Mob.Screen` GenServer is about to stop — the app exits, the process crashes, or is shut down. It is **not** called when navigation pops or resets a screen: those actions only swap the active module/socket and the stack snapshot inside the same process. So do not rely on it for per-screen cleanup; to release a screen's resources (timers, subscriptions) before it leaves the stack, do it explicitly in the callback that triggers `pop_screen/1`.

The default is a no-op. Most screens don't need to implement this.
The return value is ignored. The default is a no-op. Most screens don't need to implement this.

## Lifecycle flow

Expand All @@ -165,7 +165,7 @@ start_root/2 or push_screen/2
│ │
├── send(pid, msg) ──────► handle_info/2 ──► render/1
│ │
└── screen popped from stack ─► terminate/2 ──────┘
└── screen popped from stack ─► snapshot restored ─┘
```

## The socket
Expand Down
19 changes: 10 additions & 9 deletions lib/mob/screen.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ defmodule Mob.Screen do
@moduledoc """
Behaviour and GenServer wrapper for a Mob screen.

Each screen runs as a supervised GenServer whose state is a `Mob.Socket`.
Putting one process per screen — instead of one big process for the whole
app — gives you isolation: a buggy `handle_event` crashes its own screen
and the supervisor restarts it without taking down navigation, audio,
background services, or the BEAM itself. Lifecycle callbacks (`mount`,
`render`, `handle_event`, `handle_info`, `terminate`) map directly to the
GenServer lifecycle, so the BEAM's existing concurrency tools (selective
receive, monitors, hot code push) work on screens without any Mob-specific
scaffolding.
One `Mob.Screen` GenServer owns the whole navigation stack. The active
screen is a `{module, socket}` pair in the process state, and `nav_history`
keeps a snapshot of every pushed screen. A push mounts the next module in
this same process; a pop restores the previous snapshot without running
`mount/3` again or starting a new process. `terminate/2` therefore fires
only when the whole GenServer stops (app exit, crash, shutdown) — not when
navigation leaves a screen. All `mount`/`handle_event`/`handle_info`/
`render` callbacks run inside this one process, so the BEAM's concurrency
tools (selective receive, monitors, hot code push) apply to the app's whole
event loop.

## Usage

Expand Down