Packages
mob_new
0.3.0
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
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.45
0.1.44
0.1.43
0.1.42
0.1.40
0.1.33
0.1.30
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Project generator for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/mob.new.ex
defmodule Mix.Tasks.Mob.New do
use Mix.Task
@shortdoc "Create a new Mob mobile app project"
@moduledoc """
Creates a new Mob project with Android and iOS boilerplate.
mix mob.new APP_NAME [--liveview] [--python] [--ios | --android] [--no-install] [--dest DIR] [--local]
## Platform selection
By default the generator emits boilerplate for both Android and iOS. Pass
one of the platform flags to scope it to a single platform:
mix mob.new my_app --ios # iOS only — no android/ directory generated
mix mob.new my_app --android # Android only — no ios/ directory generated
mix mob.new my_app # both (default)
`--no-ios` and `--no-android` are equivalent inverse forms (kept for
back-compat). `mix mob.install`, `mix mob.deploy`, and `mix mob.doctor`
detect the project's platform set from on-disk layout, so a single-platform
project skips the absent platform's setup automatically.
## Options
* `--liveview` — generate a Phoenix LiveView app wrapped in a Mob WebView.
Calls `mix phx.new` to scaffold a Phoenix project, then
adds the Mob native boilerplate and LiveView bridge patches
(MobHook in app.js, mob-bridge element in root.html.heex,
MobScreen, mob.exs with liveview_port). Requires
`phx_new` archive to be installed (`mix archive.install hex phx_new`).
* `--ios` — generate iOS boilerplate only (skip android/)
* `--android` — generate Android boilerplate only (skip ios/)
* `--no-ios` — alias for `--android` (skip iOS boilerplate)
* `--no-android` — alias for `--ios` (skip Android boilerplate)
* `--python` — pre-configure embedded CPython via Pythonx (iOS only).
Adds `:pythonx` to deps, generates a
`<App>.PythonPaths` detection module, and gates
`:pythonx, :uv_init` in `config.exs` on
`MOB_TARGET=ios`. Mirrors `mix mob.enable python`
post-scaffold. Bundle size ~70 MB; iOS only —
Android Python is intentionally out of scope. See
the `Embedded CPython` guide in mob_dev's docs.
* `--no-install` — skip running `mix deps.get` after generation
* `--dest DIR` — create project in DIR (default: current directory)
* `--local` — use `path:` deps pointing to local mob/mob_dev repos
instead of hex version constraints, AND render from
the local mob_new checkout's templates (not the
installed archive's, which can be stale relative to
master). **For Mob framework contributors only** —
not intended for app developers.
Mob paths resolved from `MOB_DIR` / `MOB_DEV_DIR`
env vars, falling back to `./mob` / `./mob_dev`,
then `../mob` / `../mob_dev`. mob_new template path
resolved from `MOB_NEW_DIR`, falling back to
`$HOME/code/mob_new`. Pre-fills `mob.exs` so
`mix mob.install` skips path configuration prompts.
If the local mob_new checkout can't be found, falls
back to the installed archive's templates and notes
it in the build output.
## What gets generated (native mode, default)
APP_NAME/
mix.exs
lib/APP_NAME/app.ex
lib/APP_NAME/home_screen.ex
android/
settings.gradle
build.gradle
app/
build.gradle
src/main/
AndroidManifest.xml
java/com/mob/APP_NAME/MainActivity.kt
java/com/mob/APP_NAME/MobBridge.kt
java/com/mob/APP_NAME/MobNode.kt
java/com/mob/APP_NAME/MobScannerActivity.kt
gradle.properties
ios/
beam_main.m
Info.plist
## What gets generated (--liveview mode)
Everything `mix phx.new APP_NAME --no-install` generates, plus:
APP_NAME/
lib/APP_NAME/mob_screen.ex # Mob.Screen wrapping the Phoenix WebView
mob.exs # Mob config with liveview_port: 4000
android/ # same Android boilerplate as native mode
ios/ # same iOS boilerplate as native mode
Patches applied to the Phoenix project:
- `assets/js/app.js` — MobHook definition + registration
- `lib/APP_NAME_web/.../root.html.heex` — mob-bridge hidden div
- `lib/APP_NAME/application.ex` — Mob.App child in supervision tree
- `mix.exs` — mob / mob_dev deps added
After generation, run:
cd APP_NAME
mix mob.install # icon generation + any first-run setup
"""
@switches [
no_install: :boolean,
no_ios: :boolean,
no_android: :boolean,
ios: :boolean,
android: :boolean,
dest: :string,
local: :boolean,
liveview: :boolean,
python: :boolean
]
@impl Mix.Task
def run(argv) do
{opts, args, _} = OptionParser.parse(argv, strict: @switches)
app_name = parse_app_name!(args)
{dest_dir, liveview, gen_opts} = parse_gen_opts(opts)
project_dir = Path.join(Path.expand(dest_dir), app_name)
log_flags(gen_opts, liveview)
Mix.shell().info([:green, "* creating ", :reset, project_dir])
case generate(app_name, dest_dir, liveview, gen_opts) do
{:error, reason} -> Mix.raise(reason)
{:ok, project_dir} -> post_generate(project_dir, app_name, liveview, gen_opts, opts)
end
end
defp parse_app_name!([name | _]), do: validate_app_name!(name)
defp parse_app_name!([]), do: Mix.raise("Usage: mix mob.new APP_NAME")
defp validate_app_name!(name) do
unless valid_app_name?(name) do
Mix.raise("App name must be lowercase letters, digits, and underscores only (e.g. my_app).")
end
name
end
defp parse_gen_opts(opts) do
dest_dir = opts[:dest] || "."
liveview = opts[:liveview] || false
{no_ios, no_android} = resolve_platforms!(opts)
gen_opts = [
local: opts[:local] || false,
no_ios: no_ios,
no_android: no_android,
python: opts[:python] || false
]
{dest_dir, liveview, gen_opts}
end
# Resolves the four platform-related flags into {no_ios?, no_android?}.
# Positive flags (--ios, --android) and negative flags (--no-ios, --no-android)
# are accepted; --ios is sugar for --no-android and vice versa.
defp resolve_platforms!(opts) do
ios? = opts[:ios] == true
android? = opts[:android] == true
no_ios? = opts[:no_ios] == true or android?
no_android? = opts[:no_android] == true or ios?
if no_ios? and no_android? do
Mix.raise(
"Cannot exclude both platforms. Pass at most one of --ios, --android, --no-ios, --no-android."
)
end
{no_ios?, no_android?}
end
defp log_flags(gen_opts, liveview) do
if gen_opts[:local] do
Mix.shell().info([
:yellow,
"* local mode: path: deps for mob/mob_dev + templates from local mob_new checkout",
:reset
])
end
if gen_opts[:no_ios] do
Mix.shell().info([:yellow, "* iOS skipped — Android-only project", :reset])
end
if gen_opts[:no_android] do
Mix.shell().info([:yellow, "* Android skipped — iOS-only project", :reset])
end
if liveview do
Mix.shell().info([
:cyan,
"* --liveview: generating Phoenix LiveView app with Mob bridge",
:reset
])
end
if gen_opts[:python] do
Mix.shell().info([
:cyan,
"* --python: enabling embedded CPython (iOS only). See python_embedding guide.",
:reset
])
end
end
defp generate(app_name, dest_dir, true = _liveview, gen_opts) do
MobNew.ProjectGenerator.liveview_generate(app_name, dest_dir, gen_opts)
end
defp generate(app_name, dest_dir, false = _liveview, gen_opts) do
MobNew.ProjectGenerator.generate(app_name, dest_dir, gen_opts)
end
defp post_generate(project_dir, app_name, liveview, gen_opts, opts) do
unless liveview, do: print_created_files(project_dir, app_name, gen_opts)
unless opts[:no_install], do: fetch_deps(project_dir)
if liveview do
print_liveview_next_steps(app_name, opts[:no_install])
else
print_next_steps(app_name, opts[:no_install], gen_opts)
end
end
# ── private ──────────────────────────────────────────────────────────────────
defp fetch_deps(project_dir) do
Mix.shell().info("")
Mix.shell().info("Fetching dependencies...")
mix = System.find_executable("mix")
abs_dir = Path.expand(project_dir)
case System.cmd(mix, ["deps.get"], cd: abs_dir, into: IO.stream()) do
{_, 0} ->
:ok
{_, _} ->
Mix.shell().info([
:yellow,
"* deps.get failed — run it manually inside #{project_dir}",
:reset
])
end
end
defp print_created_files(project_dir, app_name, gen_opts) do
no_ios = gen_opts[:no_ios] || false
no_android = gen_opts[:no_android] || false
common = ["mix.exs", "lib/#{app_name}/app.ex", "lib/#{app_name}/home_screen.ex"]
android_files =
if no_android,
do: [],
else: [
"android/settings.gradle",
"android/build.gradle",
"android/app/build.gradle",
"android/app/src/main/AndroidManifest.xml",
"android/app/src/main/java/com/mob/#{app_name}/MainActivity.kt",
"android/app/src/main/java/com/mob/#{app_name}/MobBridge.kt",
"android/app/src/main/java/com/mob/#{app_name}/MobNode.kt",
"android/app/src/main/java/com/mob/#{app_name}/MobScannerActivity.kt",
"android/gradle.properties"
]
ios_files = if no_ios, do: [], else: ["ios/beam_main.m", "ios/Info.plist"]
Enum.each(common ++ android_files ++ ios_files, fn f ->
Mix.shell().info([:green, "* creating ", :reset, Path.join(project_dir, f)])
end)
end
defp print_next_steps(app_name, no_install, gen_opts) do
install_hint =
if no_install,
do: "\n mix deps.get",
else: ""
{paths_hint, binaries_hint} = platform_specific_hints(gen_opts)
provision_hint = ios_provision_hint(gen_opts)
Mix.shell().info("""
Your Mob app #{app_name} is ready!
#{install_hint}
cd #{app_name}
mix mob.install # generates app icon + first-run setup
#{provision_hint}
First deploy — edit mob.exs#{paths_hint}, then build native binaries
(#{binaries_hint}), install on device, and push BEAMs:
mix mob.deploy --native # first time, or after native code changes
Day-to-day development — just push changed BEAMs, no native rebuild needed:
mix mob.deploy # fast push + restart
mix mob.watch # auto-push on file save
""")
end
# Returns a `mix mob.provision` callout when the project includes iOS, or
# an empty string otherwise. Required as a one-time setup step before
# `mix mob.deploy --native` can install to a physical iPhone (registers
# the bundle ID with Apple and downloads a development provisioning
# profile). Skipped for sim-only flows since simctl install doesn't sign.
defp ios_provision_hint(gen_opts) do
if gen_opts[:no_ios] do
""
else
"""
mix mob.provision # iOS only, one-time: register bundle ID
# + download provisioning profile (skip if
# you're only running on the simulator)
"""
end
end
defp platform_specific_hints(gen_opts) do
no_ios = gen_opts[:no_ios] || false
no_android = gen_opts[:no_android] || false
case {no_android, no_ios} do
# iOS only — no Android SDK path needed
{true, false} -> {"", "iOS app"}
# Android only
{false, true} -> {" and android/local.properties with your local paths", "APK"}
# Both
_ -> {" and android/local.properties with your local paths", "APK + iOS app"}
end
end
defp print_liveview_next_steps(app_name, no_install) do
install_hint =
if no_install,
do: "\n mix deps.get",
else: ""
Mix.shell().info("""
Your Mob LiveView app #{app_name} is ready!
#{install_hint}
Next steps:
1. Edit mob.exs with your local paths (mob_dir, elixir_lib).
2. Edit android/local.properties with your Android SDK path.
3. Run first-time setup:
cd #{app_name}
mix mob.install # icon generation + first-run setup
4. Configure your database in config/dev.exs and run:
mix ecto.create && mix ecto.migrate
5. Run the Phoenix server once to download JS/CSS dependencies and compile
assets. This is required before deploying — skipping it will result in
missing assets on device:
mix phx.server
Open http://localhost:4000 in your browser to confirm it loads, then
stop the server (Ctrl-C).
6. iOS only — if you're targeting a physical iPhone, register the bundle
ID with Apple and download a provisioning profile (one-time setup;
skip for the simulator):
mix mob.provision
7. Deploy to device (first time — builds native APK/iOS app):
mix mob.deploy --native
The Mob WebView will load your Phoenix app at http://127.0.0.1:4000/.
Verify `window.mob.send` in browser devtools routes through `pushEvent`
(not `postMessage`) to confirm the LiveView bridge is active.
Day-to-day development:
mix mob.deploy # fast push + restart
mix mob.watch # auto-push on file save
""")
end
defp valid_app_name?(name) do
case name do
<<first, rest::binary>> when first in ?a..?z ->
rest
|> :binary.bin_to_list()
|> Enum.all?(fn c -> c in ?a..?z or c in ?0..?9 or c == ?_ end)
_ ->
false
end
end
end