Mob.Screen behaviour (mob v0.7.39)

Copy Markdown View Source

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", %{})

Summary

Callbacks

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).

Reconstruct assigns from a previously persisted map.

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

Functions

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

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.

Types

socket()

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

Callbacks

dump_state(assigns)

@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(event, params, socket)

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

handle_info(message, socket)

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

load_state(stored_vsn, stored)

@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(params, session, socket)

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

render(assigns)

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

screen_key(assigns)

(optional)
@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(reason, socket)

(optional)
@callback terminate(reason :: term(), socket :: socket()) :: term()

Functions

dispatch(pid, event, params)

@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(pid)

@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(pid)

@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(pid)

@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(pid)

@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(screen_module, params, opts \\ [])

@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(screen_module, params \\ %{}, opts \\ [])

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