From 30faaac12701641abff3681a34c12122b7ad183c Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sun, 9 Aug 2026 01:32:04 +0700 Subject: [PATCH] docs: align screen lifecycle docs with single-GenServer navigation guides/screen_lifecycle.md, navigation.md, Mob.Screen moduledoc, and PLAN.md claimed each screen in the nav stack is a separate supervised process and that popping calls terminate/2. In reality one Mob.Screen GenServer owns the whole stack: push mounts in the same process, pop restores a snapshot from nav_history. terminate/2 fires only when the whole GenServer stops. Rewrite the affected sections to match the shipped model. --- PLAN.md | 2 +- guides/navigation.md | 2 +- guides/screen_lifecycle.md | 10 +++++----- lib/mob/screen.ex | 19 ++++++++++--------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/PLAN.md b/PLAN.md index 6776684..fab62b0 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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` diff --git a/guides/navigation.md b/guides/navigation.md index 2a555ea..3b11773 100644 --- a/guides/navigation.md +++ b/guides/navigation.md @@ -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: diff --git a/guides/screen_lifecycle.md b/guides/screen_lifecycle.md index cb0ee41..b626ca2 100644 --- a/guides/screen_lifecycle.md +++ b/guides/screen_lifecycle.md @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/lib/mob/screen.ex b/lib/mob/screen.ex index 5ebe771..13a2e71 100644 --- a/lib/mob/screen.ex +++ b/lib/mob/screen.ex @@ -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