# `Mob.Screen`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/screen.ex#L1)

Behaviour and GenServer wrapper for a Mob screen.

Each live screen runs in its own `Mob.Screen.Server` process holding a
`Mob.Socket`. `Mob.Router` owns them: it holds the navigation state, starts
and stops screens, and restarts one that crashes.

That gives you isolation — a buggy `handle_event` crashes its own screen and
the router restarts it without taking down navigation, sibling screens,
background services, or the BEAM. The router is not an OTP `Supervisor`; it
restarts screens itself because only it knows where in the navigation a
crashed screen sat (see
`decisions/2026-08-28-screen-processes-and-supervision.md`). A restarted
screen re-mounts and loses its assigns.

The functions below delegate to `Mob.Router`; this module is the behaviour
your screens implement.

Lifecycle callbacks (`mount`, `render`, `handle_event`, `handle_info`,
`terminate`) map directly to the GenServer lifecycle, so the BEAM's existing
tools (selective receive, monitors, hot code push) work on screens without
any Mob-specific scaffolding.

## Usage

    defmodule MyApp.CounterScreen do
      use Mob.Screen

      def mount(_params, _session, socket) do
        {:ok, Mob.Socket.assign(socket, :count, 0)}
      end

      def render(assigns) do
        %{
          type: :column,
          props: %{},
          children: [
            %{type: :text, props: %{text: "Count: #{assigns.count}"}, children: []}
          ]
        }
      end

      def handle_event("increment", _params, socket) do
        {:noreply, Mob.Socket.assign(socket, :count, socket.assigns.count + 1)}
      end
    end

## Starting a screen

    {:ok, pid} = Mob.Screen.start_link(MyApp.CounterScreen, %{})

## Dispatching events

    :ok = Mob.Screen.dispatch(pid, "increment", %{})

# `socket`

```elixir
@type socket() :: Mob.Socket.t()
```

# `dump_state`

```elixir
@callback dump_state(assigns :: map()) :: map()
```

Serialise assigns for persistence. Return a plain map of the keys you want
restored on next launch. Defaults to the full assigns map minus any
non-serialisable values (PIDs, references, ports, functions).

Only called when `use Mob.Screen, vsn: N` (N > 0) or `persist: true`.

# `handle_event`
*optional* 

```elixir
@callback handle_event(event :: String.t(), params :: map(), socket :: socket()) ::
  {:noreply, socket()} | {:reply, map(), socket()}
```

# `handle_info`
*optional* 

```elixir
@callback handle_info(message :: term(), socket :: socket()) :: {:noreply, socket()}
```

# `load_state`

```elixir
@callback load_state(stored_vsn :: non_neg_integer(), stored :: map()) :: map()
```

Reconstruct assigns from a previously persisted map.

`stored_vsn` is the version that was current when the data was saved.
Match on it to migrate old shapes:

    def load_state(1, stored), do: stored
    def load_state(0, stored), do: Map.put(stored, :new_field, :default)

The returned map is merged into the socket's assigns after `mount/3` runs.
Only called when stored data exists.

# `mount`

```elixir
@callback mount(params :: map(), session :: map(), socket :: socket()) ::
  {:ok, socket()} | {:error, term()}
```

# `render`

```elixir
@callback render(assigns :: map()) :: map()
```

# `screen_key`
*optional* 

```elixir
@callback screen_key(assigns :: map()) :: String.t()
```

Return a stable string key for storing this screen's state.

Implement when the same screen module holds per-user or parameterised state:

    def screen_key(assigns), do: "#{__MODULE__}:#{assigns.user_id}"

Defaults to the module name string.

# `terminate`
*optional* 

```elixir
@callback terminate(reason :: term(), socket :: socket()) :: term()
```

# `dispatch`

```elixir
@spec dispatch(pid(), String.t(), map()) :: :ok
```

Dispatch a UI event to the screen process. Returns `:ok` synchronously once
the event has been processed and the state updated.

# `get_current_module`

```elixir
@spec get_current_module(pid()) :: module()
```

Return the module of the currently active screen in the navigation stack.
Intended for testing and debugging.

# `get_nav_history`

```elixir
@spec get_nav_history(pid()) :: [{module(), Mob.Socket.t() | nil}]
```

Return the navigation history (list of `{module, socket}` pairs, head = most recent).
Intended for testing and debugging.

# `get_screen_pid`

```elixir
@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.

# `get_socket`

```elixir
@spec get_socket(pid()) :: socket() | 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.

# `start_link`

```elixir
@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`.

# `start_root`

```elixir
@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).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
