Packages
mob
0.9.21
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.39
0.7.38
0.7.37
0.7.36
0.7.35
0.7.34
0.7.33
0.7.32
0.7.31
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
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
guides/getting_started.md
# Getting Started
Mob runs on iOS and Android. Pick your target — you don't need both.
→ [iOS only](#ios-only)
→ [Android only](#android-only)
→ [Both platforms](#both-platforms)
→ [LiveView projects](#liveview-projects)
---
## iOS only
### What you need
- macOS
- Xcode 15 or later (`xcode-select --install` for command-line tools)
- Elixir 1.19 or later with Hex: `mix local.hex`
- `mob_new` installed: `mix archive.install hex mob_new`
That's enough to run on the **iOS Simulator**. For a **physical iPhone**, you also need:
- An Apple ID — free at https://appleid.apple.com
- Xcode signed in with that Apple ID: open Xcode → Settings → Accounts → [+]
- (For App Store distribution only) Apple Developer Program — $99/year.
Free accounts can deploy to your own devices; profiles expire every 7 days.
### Create a project
```bash
mix mob.new my_app --ios
cd my_app
mix mob.install
```
`--ios` scopes the generator to iOS only — no `android/` directory is created
and `mix mob.install` skips the Android OTP download (saves ~400 MB of cache).
Drop the flag if you want both platforms.
`mix mob.install` downloads the pre-built OTP runtime for iOS and writes your `mob.exs`.
### Verify your environment
```bash
mix mob.doctor
```
Fix any failures before continuing. See [Troubleshooting](troubleshooting.md) if needed.
### Run on the iOS Simulator
Boot a simulator from Xcode → Open Simulator, or:
```bash
xcrun simctl boot "iPhone 16 Pro"
open -a Simulator
```
Then deploy:
```bash
mix mob.deploy --native --ios
```
This builds the `.app` bundle, installs it in the simulator, and pushes your BEAM files.
Subsequent deploys without `--native` are faster — they push only changed `.beam` files:
```bash
mix mob.deploy --ios
```
### Run on a physical iPhone
There are three one-time steps before your first deploy. Do them in order.
#### Step 1 — Trust the Mac
Connect your iPhone to your Mac with a USB cable. The phone will show:
> **"Trust This Computer?"**
Tap **Trust**, then enter your passcode. If this dialog doesn't appear, unplug and
replug the cable, or try a different port.
#### Step 2 — Enable Developer Mode on the iPhone
This is required on iOS 16 and later. You only do it once per phone.
On the iPhone: **Settings → Privacy & Security → Developer Mode → turn ON**
The phone will warn you and ask to restart. Tap **Restart**. After it reboots, a
dialog appears asking to confirm — tap **Enable** and enter your passcode.
> If you don't see Developer Mode in Settings, make sure the phone is connected to
> the Mac and Xcode is open. Xcode needs to recognise the device at least once for
> the option to appear.
#### Step 3 — Register your app ID and get a provisioning profile
Apple requires every app installed on a physical device to be signed with a
provisioning profile tied to your developer account. Run:
```bash
mix mob.provision
```
This generates a small Xcode project stub, uses it to register your bundle ID with
Apple, and downloads a development provisioning profile — all from the command line.
You won't need to build or launch anything in Xcode.
> **If `mix mob.provision` fails:** open Xcode, select your phone from the device
> picker at the top, and wait for it to finish "Preparing device for development".
> Then re-run `mix mob.provision`.
#### Deploy
```bash
mix mob.deploy --native --ios
```
Mob auto-detects the connected phone. The app will appear on your home screen.
If the profile expires (free accounts: every 7 days, paid Developer Program: 1 year),
re-run `mix mob.provision` then deploy again.
---
## Android only
### What you need
- Elixir 1.19 or later with Hex: `mix local.hex`
- `mob_new` installed: `mix archive.install hex mob_new`
- Java 17–21 (`brew install --cask temurin` on macOS)
- Android Studio (includes the SDK and `adb`)
For a **physical Android device**: enable Developer Options and USB Debugging on the
device, then connect via USB and accept the authorization prompt.
### Create a project
```bash
mix mob.new my_app --android
cd my_app
mix mob.install
```
`--android` scopes the generator to Android only — no `ios/` directory is created
and `mix mob.install` skips the iOS OTP download. Drop the flag if you want both
platforms.
`mix mob.install` downloads the pre-built OTP runtime for Android and writes your
`mob.exs` and `android/local.properties`.
### Verify your environment
```bash
mix mob.doctor
```
Fix any failures before continuing. See [Troubleshooting](troubleshooting.md) if needed.
### Run on an emulator
Start an AVD from Android Studio → Device Manager, then:
```bash
mix mob.deploy --native --android
```
This builds the APK, installs it on the emulator, and pushes your BEAM files.
Subsequent deploys without `--native` push only changed `.beam` files:
```bash
mix mob.deploy --android
```
### Run on a physical Android device
There are two one-time steps before your first deploy.
#### Step 1 — Enable Developer Mode on the phone
Android hides developer options until you unlock them.
1. Open **Settings → About phone**
- On Samsung: **Settings → About phone → Software information**
2. Find **Build number** and tap it **7 times** in quick succession
3. You'll see: *"You are now a developer!"*
4. Go back to **Settings** — a new **Developer options** entry has appeared
5. Open **Developer options** and turn on **USB debugging**
#### Step 2 — Connect via USB and set the mode
Plug in the USB cable. On the phone:
- A prompt appears: **"Allow USB debugging?"** — tap **Allow**
(tick "Always allow from this computer" so you're not asked every time)
- Pull down the notification shade and tap the USB connection notification
- Select **File Transfer** (sometimes labelled "MTP") — not "Charging only"
Verify the connection:
```bash
adb devices
```
You should see your device listed as `device` (not `unauthorized` or `offline`).
If it shows `unauthorized`, check for a missed dialog on the phone screen.
#### Deploy
```bash
mix mob.deploy --native --android
```
Mob detects connected devices automatically. If you have more than one, use
`mix mob.devices` to find the serial ID and `--device <id>` to target it.
---
## Both platforms
### What you need
Everything from the iOS and Android sections above — but you don't need a physical
device for both. Mix and match whatever you actually have:
| Setup | What to do |
|-------|-----------|
| iOS Simulator + Android emulator | Nothing extra — just deploy |
| Physical iPhone + Android emulator | Set up iPhone (trust + Developer Mode + `mix mob.provision`) |
| iOS Simulator + physical Android | Set up Android (Developer Mode + USB debugging + File Transfer) |
| Physical iPhone + physical Android | Set up both phones, then `mix mob.provision` for iOS |
### Create a project
```bash
mix mob.new my_app
cd my_app
mix mob.install
```
### Verify your environment
```bash
mix mob.doctor
```
### Deploy to everything you have connected
```bash
mix mob.deploy --native
```
Without `--ios` or `--android`, Mob targets all connected simulators, emulators, and
physical devices at once — whatever is available. On macOS it includes both platforms;
on Linux/Windows it deploys Android only. You don't need to tell it what you have.
### If you have a physical iPhone (one-time setup)
Before your first deploy to a physical iPhone, register your app with Apple:
```bash
mix mob.provision
```
Then deploy normally — Mob auto-detects the phone alongside any running simulators
or emulators and pushes to all of them in one command:
```bash
mix mob.deploy --native
```
If you only want to target the phone and skip the simulators for a deploy:
```bash
mix mob.deploy --native --ios
```
### Targeting one device at a time
Use `mix mob.devices` to see what's connected and their IDs, then `--device <id>` to
target a specific one — useful when you have multiple physical devices or want to
isolate a deploy while keeping others running.
---
## LiveView projects
Instead of writing screens in Elixir with the `~MOB` sigil, you can run a full
Phoenix LiveView app inside a native WebView. The native shell handles device
APIs and distribution; your UI is a regular Phoenix web app.
The first-run flow is **not the same** as a native project — it has database
setup, an extra asset-pipeline step, and a couple of paths to fill in by hand.
The full sequence is below.
### Mixed apps are fine
You don't have to pick one mode for the whole app. A native Mob project can
host LiveView screens (run `mix mob.enable liveview` in an existing project),
and a LiveView project can include native `Mob.Screen` modules alongside its
WebView screens. Use whichever fits each part of the app.
One thing to be aware of: a mixed app has **two distinct forms of navigation**.
* **Phoenix routes** — `live "/foo", FooLive` in `router.ex`, navigated with
`<.link navigate={...}>` or `push_navigate(...)`. Lives entirely inside the
LiveView WebSocket; the WebView's URL changes but the native nav stack
doesn't.
* **Native navigation** — `Mob.Socket.push_screen/2,3`, `pop_screen/1`,
tab bars, drawers. Lives in the Mob navigation stack; the WebView is just
one screen on that stack.
The two stacks don't talk to each other (by default but you control both sides so if you _really_ want to you could make that happen). A Phoenix route change inside a
WebView doesn't push a native screen, and a `push_screen` doesn't navigate
the WebView. Plan crossings explicitly: a tap inside the LiveView that should
push a native screen sends a `mob_message` event up to the hosting
`Mob.Screen`, which calls `Mob.Socket.push_screen/2,3`; a native back-button in a parent
screen pops the WebView screen as a whole, not the route inside it.
### Extra prerequisite
You need the `phx_new` archive in addition to `mob_new`:
```bash
mix archive.install hex phx_new
```
### Create a LiveView project
Pass `--liveview` to `mix mob.new`:
```bash
mix mob.new my_app --liveview
cd my_app
```
`--liveview` combines with `--ios` or `--android` if you want a single-platform
LiveView project — for example `mix mob.new my_app --liveview --ios` skips
Android scaffolding entirely.
This calls `mix phx.new` under the hood, then patches the generated project:
adds the Mob bridge hook to `app.js`, inserts the `mob-bridge` element in
`root.html.heex`, adds `Mob.App` to the supervision tree, and writes a
`mob.exs` with `liveview_port: 4000`.
### 1. Configure local paths
Unlike a native project, the LiveView template doesn't auto-fill machine-
specific paths. Open these two files and set the values for your machine.
**`mob.exs`** — set both keys:
* `mob_dir` — local path to the mob library (or `deps/mob` if vendored)
* `elixir_lib` — your Elixir lib dir, e.g.
`~/.local/share/mise/installs/elixir/1.19.5-otp-28/lib`
**`android/local.properties`** — set the Android SDK path:
```
sdk.dir=/Users/you/Library/Android/sdk
```
### 2. Run first-time setup
```bash
mix mob.install
```
Caches the OTP runtimes, generates a placeholder app icon, and finalises the
build config.
### 3. Configure and create the database
Edit `config/dev.exs` to point at your dev database (the Phoenix-generated
defaults work for most local Postgres setups), then:
```bash
mix ecto.create && mix ecto.migrate
```
### 4. Run the Phoenix server once (required)
This downloads JS/CSS dependencies and compiles static assets. **Skipping
this step is the most common cause of a blank-screen first deploy** — the
WebView loads `http://127.0.0.1:4000/` but the asset pipeline has never
produced any files for it to serve.
```bash
mix phx.server
```
Open `http://localhost:4000` in your browser to confirm it loads, then stop
the server (`Ctrl-C`).
### 5. Deploy to device
```bash
mix mob.deploy --native
```
The native app starts your Phoenix server at `http://127.0.0.1:4000/` and
loads it in a WebView.
### 6. Verify the LiveView bridge
After the app launches, open the WebView in a remote inspector (Safari Web
Inspector for iOS, `chrome://inspect` for Android) and run:
```js
window.mob.send({some: 'event'})
```
The call should route through Phoenix's `pushEvent` — visible as a LiveView
event on the server side — **not** through `window.postMessage`. That
confirms the Mob ↔ LiveView bridge is wired correctly.
### Day-to-day development
The workflow is the same as a native project — push changed BEAMs, restart,
or watch for file changes:
```bash
mix mob.deploy # push BEAMs + restart (Phoenix server restarts inside the app)
mix mob.watch # auto-push on file save
```
Phoenix code changes (templates, LiveViews) are picked up automatically when
the BEAM restarts. Asset changes (`app.js`, CSS) require running
`mix assets.build` locally first, since the device runs your compiled
assets, not the dev pipeline.
### Adding LiveView to an existing native project
If you already have a Mob project (created without `--liveview`) and want to
turn it into a LiveView app, run:
```bash
mix mob.enable liveview
```
This is the same patcher that `mix mob.new --liveview` runs for new projects:
generates `lib/<app>/mob_screen.ex`, injects `MobHook` into `assets/js/app.js`,
inserts the hidden `<div id="mob-bridge">` into `root.html.heex`, sets
`liveview_port` in `mob.exs`, and adds the Android `networkSecurityConfig`
that lets the WebView reach `127.0.0.1`. See [LiveView Mode](liveview.md) for
the full architecture explanation.
---
## After the first deploy
These commands work the same regardless of platform.
### Connect a live IEx session
```bash
mix mob.connect
```
Tunnels Erlang distribution and drops you into an IEx session connected to the running
BEAM on the device. You can inspect state, call functions, and push code live.
```elixir
Node.list()
#=> [:"my_app_ios@127.0.0.1"]
Mob.Test.assigns(:"my_app_ios@127.0.0.1")
#=> %{count: 0, safe_area: %{top: 62.0, ...}}
```
### Hot-push a code change
Edit a module, then push the new bytecode without restarting:
```bash
mix mob.push
```
Changed `.beam` files are loaded directly into the running BEAM via RPC — no restart,
no state loss. The screen updates immediately.
### Auto-push on save
```bash
mix mob.watch
```
Watches for file changes and runs `mob.push` automatically. Combine with
`mix mob.connect` to keep an IEx session open alongside.
### App configuration on device
`config/config.exs` works on the device the way it does on your machine:
`Application.get_env(:my_app, :key)` returns what you configured. There's no
release on the phone, so mob_dev evaluates your config **at build time** —
`config/config.exs` for the build's Mix env and target, then
`config/runtime.exs` if you have one, also on your machine, not the phone —
and ships the result with the app. `Mob.App.start/0` puts it into the
application environment before anything else runs. The `:mob_dev` key stays
on your machine.
Three consequences worth knowing:
- `runtime.exs` sees your build machine's environment variables, not the
phone's. Don't put secrets there that you wouldn't ship inside the app.
- The values are read once, at app start. After changing config, run
`mix mob.deploy` (which restarts the app); `mix mob.push` alone doesn't
reapply it.
- Logger is already running when the config arrives. `config :logger,
level: …` and `:translator_inspect_opts` take effect; settings Logger reads
only when it starts (`:default_handler`, `:default_formatter`,
`:handle_otp_reports`, `:handle_sasl_reports`, `:translators`,
`:backends`) don't. See `Mob.AppConfig`.
The device log shows what was applied, names and key counts only:
`[mob] app config: loaded :mob_app_config (my_app: 2 keys, mob_deliver: 4 keys)`.
---
## Deployment reference
| Command | Restarts? | Requires dist? | What it does |
|---------|:---------:|:--------------:|---|
| `mix mob.deploy --native` | Yes | No | Build native binary + install + push BEAMs |
| `mix mob.deploy` | Yes | No | Push BEAMs + restart (no native rebuild) |
| `mix mob.push` | No | **Yes** | Hot-push changed BEAMs via RPC |
| `mix mob.watch` | No | **Yes** | `mob.push` on every file save |
| `nl(MyApp.Screen)` in IEx | No | **Yes** | Hot-push a single module |
**Requires dist** means Erlang distribution must be active. Run `mix mob.connect` first,
or use the dashboard (`mix mob.server`) which sets it up automatically.
### Which command should I use?
- **First time, or after changing Swift/Kotlin/C?** → `mix mob.deploy --native`
- **Changed Elixir, want a clean restart?** → `mix mob.deploy`
- **Changed Elixir, want to keep app state?** → `mix mob.push`
- **Want changes pushed automatically while editing?** → `mix mob.watch`
---
## Toolchain managers
Mob is **tested against [mise](https://mise.jdx.dev/)** for managing Elixir
and Erlang versions. The repos ship a `.tool-versions` file that mise reads
automatically.
**asdf** uses the same `.tool-versions` format and should work without
changes — install Elixir/Erlang the asdf way and you're done. We don't
actively test it, but no Mob code touches mise or asdf directly; everything
works off whatever `mix`, `elixir`, `iex`, and `erl` resolve to on your PATH.
**Nix users** need to set a few env vars yourself, since mob_dev's auto-
detection assumes mise/asdf-style on-disk layouts (e.g. `~/.local/share/mise/
installs/elixir/...`). Set them in your shell, `direnv`, or `shell.nix`
**before running `mix mob.install`** — the install step reads them and
bakes the resolved values into `mob.exs` and `android/local.properties`.
Setting them later still works (build.sh and Gradle re-read the env at
deploy time), but you'll need to edit those config files by hand.
| Env var | Read by | When to set |
|---|---|---|
| `MOB_ELIXIR_LIB` | `mob.install` (writes into `mob.exs`); iOS `build.sh` | before `mob.install` |
| `MOB_DIR` | `mix mob.new --local` (path resolution); iOS `build.sh` | before `mob.new` (only if using `--local`) |
| `MOB_DEV_DIR` | `mix mob.new --local` (path resolution) | before `mob.new` (only if using `--local`) |
| `MOB_CACHE_DIR` | OTP downloader at install + any `--native` deploy | before `mob.install` |
| `MOB_SIM_RUNTIME_DIR` | iOS `build.sh` (writer) and `mob_beam.m` (reader) | before first `mob.deploy --native` |
| `ANDROID_HOME` | `mob.install` (auto-detected, written to `local.properties`); Gradle | before `mob.install` |
| `JAVA_HOME` | Gradle | before `mob.deploy --native` |
Each var has a default if you don't set it; the table column says where
each *would* land:
* `MOB_ELIXIR_LIB` — computed from the running BEAM (mise/asdf path)
* `MOB_DIR` / `MOB_DEV_DIR` — resolves from `mob.exs` or `deps/mob`,
or sibling discovery (`./mob_dev` then `../mob_dev`)
* `MOB_CACHE_DIR` — `~/.mob/cache/`
* `MOB_SIM_RUNTIME_DIR` — `~/.mob/runtime/ios-sim/`
* `ANDROID_HOME` — read from `android/local.properties` `sdk.dir`
Quick recipe for a Nix user with Elixir from
`pkgs.beam.packages.erlang_28.elixir_1_19`. Put this in `direnv` or
`shell.nix` so it loads on `cd`:
```sh
export MOB_ELIXIR_LIB="$(elixir -e 'IO.puts(Path.dirname(to_string(:code.lib_dir(:elixir))))')"
export MOB_CACHE_DIR="$HOME/.mob/cache" # or somewhere your Nix gc-roots manage
export MOB_SIM_RUNTIME_DIR="$HOME/.mob/runtime/ios-sim"
export ANDROID_HOME="$HOME/Android/Sdk" # wherever your nixpkgs AndroidSdk lives
```
Then run the normal flow:
```sh
mix mob.new my_app --ios
cd my_app
mix mob.install # picks up the env vars, bakes them into mob.exs / local.properties
mix mob.deploy --native
```
`mix mob.cache` and `mix mob.cache --clear` know about both `MOB_CACHE_DIR`
and `MOB_SIM_RUNTIME_DIR` overrides — if you point them at a project-local
or sandbox-friendly path, that's also what cache listings and `--clear` will
target.
---
## Caches and disk usage
`mix mob.deploy` populates a few machine-wide locations outside your project tree:
- **`~/.mob/cache/`** — pre-built OTP runtimes for iOS sim, iOS device, and
Android (one per ABI). Reused across every Mob project. ~200–400 MB each.
Override with `MOB_CACHE_DIR`.
- **`~/.mob/runtime/ios-sim/`** — the OTP root that the running iOS simulator
app reads from at startup (mob_new ≥ 0.1.20). One per machine, not per
project — last project deployed wins. Override with `MOB_SIM_RUNTIME_DIR`.
Older projects use `/tmp/otp-ios-sim` instead, which `mob.cache` still lists.
- **`~/Library/Caches/elixir_make/`** (macOS) or `~/.cache/elixir_make/`
(Linux) — pre-built NIF tarballs that `exqlite` and other NIF deps download
instead of recompiling from source. Owned by `elixir_make`, not Mob.
To inspect or clear them:
```bash
mix mob.cache # show paths + sizes (read-only)
mix mob.cache --include-transitive # also show elixir_make's cache
mix mob.cache --clear # delete Mob's caches (with prompt)
mix mob.cache --clear --include-transitive # delete ours + elixir_make's
```
To relocate Mob-owned paths (sandbox-friendly for Nix or CI environments):
```bash
export MOB_CACHE_DIR=/path/to/cache # OTP runtime cache
export MOB_SIM_RUNTIME_DIR=/path/to/runtime # iOS simulator runtime
```
`mob.cache` deliberately does not touch `~/.hex`, `~/.mix`, `~/.gradle`, or
Xcode's `DerivedData` — those are shared with non-Mob projects and clearing
them via Mob would silently break unrelated work.
---
## Your first screen
```elixir
defmodule MyApp.HomeScreen do
use Mob.Screen
def mount(_params, _session, socket) do
{:ok, Mob.Socket.assign(socket, :count, 0)}
end
def render(assigns) do
~MOB"""
<Column padding={24} gap={16}>
<Text text={"Count: #{assigns.count}"} text_size={:xl} />
<Button text="Tap me" on_tap={{self(), :increment}} />
</Column>
"""
end
def handle_info({:tap, :increment}, socket) do
{:noreply, Mob.Socket.assign(socket, :count, socket.assigns.count + 1)}
end
def handle_info(_message, socket), do: {:noreply, socket}
end
```
`mount/3` initialises assigns. `render/1` returns the component tree via the `~MOB`
sigil. An `on_tap={{self(), :increment}}` sends `{:tap, :increment}` to the screen
when the button is pressed, which `handle_info/2` matches to update state. After each
update, the framework calls `render/1` again and pushes the diff to the native layer.
---
## Next steps
- [Screen Lifecycle](screen_lifecycle.md) — mount, render, handle_event, handle_info
- [Components](components.md) — full component reference
- [Navigation](navigation.md) — stack, tab bar, drawer, push/pop
- [Theming](theming.md) — color tokens, named themes, runtime switching
- [Data & Persistence](data.md) — `Mob.State` for preferences, Ecto + SQLite for structured data
- [Device Capabilities](device_capabilities.md) — camera, location, haptics, notifications
- [Background Execution](background_execution.md) — push wakeups, foreground services, and OS background limits
- [LiveView Mode](liveview.md) — full Phoenix LiveView app inside a native WebView (the two-bridge architecture, `mix mob.enable liveview`)
- [Testing](testing.md) — unit tests and live device inspection
- [Troubleshooting](troubleshooting.md) — if something isn't working, start here