Packages
mob
0.5.14
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.2
0.6.1
0.6.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.11
0.5.10
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
BEAM-on-device mobile framework for Elixir
Current section
Files
Jump to
Current section
Files
README.md
# Mob
BEAM-on-device mobile framework for Elixir. OTP runs inside your iOS and Android apps — embedded directly in the app bundle, no server required. Screens are GenServers; the UI is rendered by Compose and SwiftUI via a thin NIF.
[](https://hex.pm/packages/mob)
[](https://hexdocs.pm/mob)
> **Status:** Early development. Android emulator and iOS simulator confirmed working. Not yet ready for production use.
## What it is
```
Your Elixir app (GenServers, OTP supervision, pattern matching, pipes)
↓
Mob.Screen (GenServer — your logic lives here)
↓
Mob.Renderer (component tree → JSON → NIF call)
↓
Compose (Android) SwiftUI (iOS) ← native rendering, native gestures
```
You write Elixir. The native layer handles rendering. The BEAM node runs on the device — connect your dev machine to the running app over Erlang distribution, inspect state, and hot-push new bytecode without a restart.
## Installation
Add to `mix.exs`:
```elixir
def deps do
[{:mob, "~> 0.5"}]
end
```
The `mob_new` package (separate) provides project generation, deployment tooling, and will import `mob_dev` which is a live dashboard. Install it as a Mix archive:
```bash
mix archive.install hex mob_new
```
## A screen
```elixir
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: %{padding: :space_md, gap: :space_md, background: :background},
children: [
%{type: :text, props: %{text: "Count: #{assigns.count}", text_size: :xl, text_color: :on_background}, children: []},
%{type: :button, props: %{text: "Increment", on_tap: {self(), :increment}}, children: []}
]
}
end
def handle_event("tap", %{"tag" => "increment"}, socket) do
{:noreply, Mob.Socket.assign(socket, :count, socket.assigns.count + 1)}
end
end
```
## App entry point
```elixir
defmodule MyApp do
use Mob.App, theme: Mob.Theme.Obsidian
def navigation(_platform) do
stack(:home, root: MyApp.CounterScreen)
end
def on_start do
Mob.Screen.start_root(MyApp.CounterScreen)
Mob.Dist.ensure_started(node: :"my_app@127.0.0.1", cookie: :secret)
end
end
```
## Navigation
```elixir
# Push a new screen
Mob.Socket.push_screen(socket, MyApp.DetailScreen, %{id: 42})
# Pop back
Mob.Socket.pop_screen(socket)
# Tab bar layout
tab_bar([
stack(:home, root: MyApp.HomeScreen, title: "Home"),
stack(:profile, root: MyApp.ProfileScreen, title: "Profile")
])
```
## Theming
```elixir
# Named theme
use Mob.App, theme: Mob.Theme.Obsidian
# Override individual tokens
use Mob.App, theme: {Mob.Theme.Obsidian, primary: :rose_500}
# From scratch
use Mob.App, theme: [primary: :emerald_500, background: :gray_950]
# Runtime switch (accessibility, user preference)
Mob.Theme.set(Mob.Theme.Citrus)
```
Built-in themes: `Mob.Theme.Obsidian` (dark violet), `Mob.Theme.Citrus` (warm charcoal + lime), `Mob.Theme.Birch` (warm parchment).
## Device APIs
All async — call the function, handle the result in `handle_info/2`:
```elixir
# Haptic feedback (synchronous — no handle_info needed)
Mob.Haptic.trigger(socket, :success)
# Camera
Mob.Camera.capture_photo(socket)
def handle_info({:camera, :photo, %{path: path}}, socket), do: ...
# Location
Mob.Location.start(socket, accuracy: :high)
def handle_info({:location, %{lat: lat, lon: lon}}, socket), do: ...
# Push notifications
Mob.Notify.register_push(socket)
def handle_info({:push_token, :ios, token}, socket), do: ...
```
Also: `Mob.Clipboard`, `Mob.Share`, `Mob.Photos`, `Mob.Files`, `Mob.Audio`, `Mob.Motion`, `Mob.Biometric`, `Mob.Scanner`, `Mob.Permissions`.
## Live development
```bash
mix mob.connect # tunnel + connect IEx to running device
nl(MyApp.SomeScreen) # hot-push new bytecode, no restart
# In IEx:
Mob.Test.screen(:"my_app_ios@127.0.0.1") #=> MyApp.CounterScreen
Mob.Test.assigns(:"my_app_ios@127.0.0.1") #=> %{count: 3, ...}
Mob.Test.tap(:"my_app_ios@127.0.0.1", :increment)
```
## Testing
```elixir
test "increments count" do
{:ok, pid} = Mob.Screen.start_link(MyApp.CounterScreen, %{})
:ok = Mob.Screen.dispatch(pid, "tap", %{"tag" => "increment"})
assert Mob.Screen.get_socket(pid).assigns.count == 1
end
```
## Related packages
| Package | Purpose |
|---------|---------|
| [`mob_dev`](https://hex.pm/packages/mob_dev) | Dev tooling: `mix mob.new`, `mix mob.deploy`, `mix mob.connect`, live dashboard |
| [`mob_push`](https://hex.pm/packages/mob_push) | Server-side push notifications (APNs + FCM) |
## Documentation
Full documentation at [hexdocs.pm/mob](https://hexdocs.pm/mob), including:
- [Getting Started](https://hexdocs.pm/mob/getting_started.html)
- [Architecture & Prior Art](https://hexdocs.pm/mob/architecture.html) — comparison to LiveView Native, Elixir Desktop, React Native, Flutter, and native development
- [Screen Lifecycle](https://hexdocs.pm/mob/screen_lifecycle.html)
- [Components](https://hexdocs.pm/mob/components.html)
- [Theming](https://hexdocs.pm/mob/theming.html)
- [Navigation](https://hexdocs.pm/mob/navigation.html)
- [Device Capabilities](https://hexdocs.pm/mob/device_capabilities.html)
- [Testing](https://hexdocs.pm/mob/testing.html)
## License
MIT