Dala runs on iOS and Android. Pick your target — you don't need both.
→ iOS only → Android only → Both platforms → LiveView projects
iOS only
What you need
- macOS
- Xcode 15 or later (
xcode-select --installfor command-line tools) - Elixir 1.19 or later with Hex:
mix local.hex dala_newinstalled:mix archive.install hex dala_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
mix dala.new my_app --ios
cd my_app
mix dala.install
--ios scopes the generator to iOS only — no android/ directory is created
and mix dala.install skips the Android OTP download (saves ~400 MB of cache).
Drop the flag if you want both platforms.
mix dala.install downloads the pre-built OTP runtime for iOS and writes your dala.exs.
Verify your environment
mix dala.doctor
Fix any failures before continuing. See Troubleshooting if needed.
Run on the iOS Simulator
Boot a simulator from Xcode → Open Simulator, or:
xcrun simctl boot "iPhone 16 Pro"
open -a Simulator
Then deploy:
mix dala.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:
mix dala.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:
mix dala.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 dala.provisionfails: open Xcode, select your phone from the device picker at the top, and wait for it to finish "Preparing device for development". Then re-runmix dala.provision.
Deploy
mix dala.deploy --native --ios
Dala 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 dala.provision then deploy again.
Android only
What you need
- Elixir 1.19 or later with Hex:
mix local.hex dala_newinstalled:mix archive.install hex dala_new- Java 17–21 (
brew install --cask temurinon 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
mix dala.new my_app --android
cd my_app
mix dala.install
--android scopes the generator to Android only — no ios/ directory is created
and mix dala.install skips the iOS OTP download. Drop the flag if you want both
platforms.
mix dala.install downloads the pre-built OTP runtime for Android and writes your
dala.exs and android/local.properties.
Verify your environment
mix dala.doctor
Fix any failures before continuing. See Troubleshooting if needed.
Run on an emulator
Start an AVD from Android Studio → Device Manager, then:
mix dala.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:
mix dala.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.
- Open Settings → About phone
- On Samsung: Settings → About phone → Software information
- Find Build number and tap it 7 times in quick succession
- You'll see: "You are now a developer!"
- Go back to Settings — a new Developer options entry has appeared
- 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:
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
mix dala.deploy --native --android
Dala detects connected devices automatically. If you have more than one, use
mix dala.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 dala.provision) |
| iOS Simulator + physical Android | Set up Android (Developer Mode + USB debugging + File Transfer) |
| Physical iPhone + physical Android | Set up both phones, then mix dala.provision for iOS |
Create a project
mix dala.new my_app
cd my_app
mix dala.install
Verify your environment
mix dala.doctor
Deploy to everything you have connected
mix dala.deploy --native
Without --ios or --android, Dala 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:
mix dala.provision
Then deploy normally — Dala auto-detects the phone alongside any running simulators or emulators and pushes to all of them in one command:
mix dala.deploy --native
If you only want to target the phone and skip the simulators for a deploy:
mix dala.deploy --native --ios
Targeting one device at a time
Use mix dala.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 ~dala 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 Dala project can
host LiveView screens (run mix dala.enable liveview in an existing project),
and a LiveView project can include native Dala.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", FooLiveinrouter.ex, navigated with<.link navigate={...}>orpush_navigate(...). Lives entirely inside the LiveView WebSocket; the WebView's URL changes but the native nav stack doesn't. - Native navigation —
Dala.Nav.push/2,pop/1, tab bars, drawers. Lives in the native nav controller; 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 Dala.Nav.push doesn't navigate
the WebView. Plan crossings explicitly: a tap inside the LiveView that should
push a native screen sends a dala_message event up to the hosting
Dala.Screen, which calls Dala.Nav.push/2; 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 dala_new:
mix archive.install hex phx_new
Create a LiveView project
Pass --liveview to mix dala.new:
mix dala.new my_app --liveview
cd my_app
--liveview combines with --ios or --android if you want a single-platform
LiveView project — for example mix dala.new my_app --liveview --ios skips
Android scaffolding entirely.
This calls mix phx.new under the hood, then patches the generated project:
adds the Dala bridge hook to app.js, inserts the dala-bridge element in
root.html.heex, adds Dala.App to the supervision tree, and writes a
dala.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.
dala.exs — set both keys:
dala_dir— local path to the dala library (ordeps/dalaif 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/sdk2. Run first-time setup
mix dala.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:
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.
mix phx.server
Open http://localhost:4000 in your browser to confirm it loads, then stop
the server (Ctrl-C).
5. Deploy to device
mix dala.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:
window.dala.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 Dala ↔ 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:
mix dala.deploy # push BEAMs + restart (Phoenix server restarts inside the app)
mix dala.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 Dala project (created without --liveview) and want to
turn it into a LiveView app, run:
mix dala.enable liveview
This is the same patcher that mix dala.new --liveview runs for new projects:
generates lib/<app>/dala_screen.ex, injects DalaHook into assets/js/app.js,
inserts the hidden <div id="dala-bridge"> into root.html.heex, sets
liveview_port in dala.exs, and adds the Android networkSecurityConfig
that lets the WebView reach 127.0.0.1. See LiveView Mode for
the full architecture explanation.
After the first deploy
These commands work the same regardless of platform.
Connect a live IEx session
mix dala.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.
Node.list()
#=> [:"my_app_ios@127.0.0.1"]
Dala.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:
mix dala.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
mix dala.watch
Watches for file changes and runs dala.push automatically. Combine with
mix dala.connect to keep an IEx session open alongside.
Deployment reference
| Command | Restarts? | Requires dist? | What it does |
|---|---|---|---|
mix dala.deploy --native | Yes | No | Build native binary + install + push BEAMs |
mix dala.deploy | Yes | No | Push BEAMs + restart (no native rebuild) |
mix dala.push | No | Yes | Hot-push changed BEAMs via RPC |
mix dala.watch | No | Yes | dala.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 dala.connect first,
or use the dashboard (mix dala.server) which sets it up automatically.
Which command should I use?
- First time, or after changing Swift/Kotlin/C? →
mix dala.deploy --native - Changed Elixir, want a clean restart? →
mix dala.deploy - Changed Elixir, want to keep app state? →
mix dala.push - Want changes pushed automatically while editing? →
mix dala.watch
Toolchain managers
Dala is tested against mise 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 Dala 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 dala_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 dala.install — the install step reads them and
bakes the resolved values into dala.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 |
|---|---|---|
dala_ELIXIR_LIB | dala.install (writes into dala.exs); iOS build.sh | before dala.install |
dala_DIR | mix dala.new --local (path resolution); iOS build.sh | before dala.new (only if using --local) |
dala_DEV_DIR | mix dala.new --local (path resolution) | before dala.new (only if using --local) |
dala_CACHE_DIR | OTP downloader at install + any --native deploy | before dala.install |
dala_SIM_RUNTIME_DIR | iOS build.sh (writer) and dala_beam.m (reader) | before first dala.deploy --native |
ANDROID_HOME | dala.install (auto-detected, written to local.properties); Gradle | before dala.install |
JAVA_HOME | Gradle | before dala.deploy --native |
Each var has a default if you don't set it; the table column says where each would land:
dala_ELIXIR_LIB— computed from the running BEAM (mise/asdf path)dala_DIR/dala_DEV_DIR— resolves fromdala.exsordeps/dala, or sibling discovery (./dala_devthen../dala_dev)dala_CACHE_DIR—~/.dala/cache/dala_SIM_RUNTIME_DIR—~/.dala/runtime/ios-sim/ANDROID_HOME— read fromandroid/local.propertiessdk.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:
export dala_ELIXIR_LIB="$(elixir -e 'IO.puts(Path.dirname(to_string(:code.lib_dir(:elixir))))')"
export dala_CACHE_DIR="$HOME/.dala/cache" # or somewhere your Nix gc-roots manage
export dala_SIM_RUNTIME_DIR="$HOME/.dala/runtime/ios-sim"
export ANDROID_HOME="$HOME/Android/Sdk" # wherever your nixpkgs AndroidSdk lives
Then run the normal flow:
mix dala.new my_app --ios
cd my_app
mix dala.install # picks up the env vars, bakes them into dala.exs / local.properties
mix dala.deploy --native
mix dala.cache and mix dala.cache --clear know about both dala_CACHE_DIR
and dala_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 dala.deploy populates a few machine-wide locations outside your project tree:
~/.dala/cache/— pre-built OTP runtimes for iOS sim, iOS device, and Android (one per ABI). Reused across every Dala project. ~200–400 MB each. Override withdala_CACHE_DIR.~/.dala/runtime/ios-sim/— the OTP root that the running iOS simulator app reads from at startup (dala_new ≥ 0.1.20). One per machine, not per project — last project deployed wins. Override withdala_SIM_RUNTIME_DIR. Older projects use/tmp/otp-ios-siminstead, whichdala.cachestill lists.~/Library/Caches/elixir_make/(macOS) or~/.cache/elixir_make/(Linux) — pre-built NIF tarballs thatexqliteand other NIF deps download instead of recompiling from source. Owned byelixir_make, not Dala.
To inspect or clear them:
mix dala.cache # show paths + sizes (read-only)
mix dala.cache --include-transitive # also show elixir_make's cache
mix dala.cache --clear # delete Dala's caches (with prompt)
mix dala.cache --clear --include-transitive # delete ours + elixir_make's
To relocate Dala-owned paths (sandbox-friendly for Nix or CI environments):
export dala_CACHE_DIR=/path/to/cache # OTP runtime cache
export dala_SIM_RUNTIME_DIR=/path/to/runtime # iOS simulator runtime
dala.cache deliberately does not touch ~/.hex, ~/.mix, ~/.gradle, or
Xcode's DerivedData — those are shared with non-Dala projects and clearing
them via Dala would silently break unrelated work.
Your first screen
defmodule MyApp.HomeScreen do
use Dala.Screen
def mount(_params, _session, socket) do
{:ok, Dala.Socket.assign(socket, :count, 0)}
end
def render(assigns) do
~dala"""
<Column padding={24} gap={16}>
<Text text={"Count: #{assigns.count}"} text_size={:xl} />
<Button text="Tap me" on_tap={tap(:increment)} />
</Column>
"""
end
def handle_info({:tap, :increment}, socket) do
{:noreply, Dala.Socket.assign(socket, :count, socket.assigns.count + 1)}
end
def handle_info(_message, socket), do: {:noreply, socket}
endmount/3 initialises assigns. render/1 returns the component tree via the ~dala
sigil. handle_info/2 updates state in response to user events. After each update,
the framework calls render/1 again and pushes the diff to the native layer.
Next steps
- Screen Lifecycle — mount, render, handle_event, handle_info
- Components — full component reference
- Navigation — stack, tab bar, drawer, push/pop
- Theming — color tokens, named themes, runtime switching
- Data & Persistence —
Dala.Statefor preferences, Ecto + SQLite for structured data - Device Capabilities — camera, location, haptics, notifications
- LiveView Mode — full Phoenix LiveView app inside a native WebView (the two-bridge architecture,
mix dala.enable liveview) - Testing — unit tests and live device inspection
- Troubleshooting — if something isn't working, start here