Mob — BEAM-on-device mobile framework for Elixir.

OTP runs on the device. Screens are GenServers. The UI is rendered by Compose (Android) and SwiftUI (iOS) via a thin NIF. No server required.

Quick start

defmodule MyApp.HomeScreen do
  use Mob.Screen

  def mount(_params, _session, socket) do
    {:ok, Mob.Socket.assign(socket, :title, "Hello, Mob!")}
  end

  def render(assigns) do
    %{
      type:  :column,
      props: %{padding: :space_md},
      children: [
        %{type: :text, props: %{text: assigns.title, text_size: :xl}, children: []}
      ]
    }
  end
end

Modules

  • Mob.App — app entry point and navigation declaration
  • Mob.Screen — screen behaviour and GenServer wrapper
  • Mob.Socket — assigns and navigation API
  • Mob.Theme — design token system
  • Mob.Renderer — component tree serialisation
  • Mob.Test — live device inspection and testing helpers

See the Getting Started guide to create your first app. See Architecture & Prior Art for how Mob compares to LiveView Native, Elixir Desktop, React Native, Flutter, and native development.

Summary

Functions

A writable, app-private directory for runtime data — DB files, caches, downloaded assets, anything you write at runtime.

Like data_dir/0 but returns (and creates) the sub directory beneath it, e.g. Mob.data_dir("audio_cache").

Functions

assign(socket, kw)

See Mob.Socket.assign/2.

assign(socket, key, value)

See Mob.Socket.assign/3.

data_dir()

@spec data_dir() :: String.t()

A writable, app-private directory for runtime data — DB files, caches, downloaded assets, anything you write at runtime.

On device this is MOB_DATA_DIR, set by the BEAM launcher to the platform's persistent app-private location (iOS NSDocumentDirectory, Android getFilesDir()). Off device (host/dev/tests) it falls back to $HOME, then the current working directory. The directory is created if missing.

Use this — not MOB_BEAMS_DIR. MOB_BEAMS_DIR points inside the signed, read-only .app bundle on iOS, so writing there fails with :eperm; it happens to be writable on Android, which is how that trap stays hidden until an app ships to iOS.

path = Path.join(Mob.data_dir(), "my.db")

See data_dir/1 for a created subdirectory.

data_dir(sub)

@spec data_dir(String.t()) :: String.t()

Like data_dir/0 but returns (and creates) the sub directory beneath it, e.g. Mob.data_dir("audio_cache").