Local Development¶
How to run the native iOS and Android apps locally. Use this when you need fast iteration — code change to running app in seconds rather than minutes.
Two local-dev paths exist:
- Expo Go — covered in Frontend → Setup. Quickest start; works for everything that doesn't need native modules beyond Expo's standard set.
expo run:android/expo run:ios— native development build on simulator/emulator/device. Required when you've added a native module that Expo Go doesn't include (Google Sign-In, native maps with Google Maps SDK, etc.). The binary is built once and the JS bundle hot-reloads against it; rebuild only on a native change.
This page covers path 2 on macOS, since Tomoda's primary native dev surface is Android (more common configuration friction). iOS local dev is shorter — mostly Xcode + a single command — and gets a brief section at the end.
Android — toolchain¶
You need three things working in concert: Java, the Android SDK, and an emulator (or a tethered physical device).
| Tool | Required version | How to install |
|---|---|---|
| JDK | OpenJDK 17 | brew install openjdk@17 |
| Android Studio | Latest stable | Download |
| Android SDK Platform-Tools | Latest | Android Studio → SDK Manager → SDK Tools tab |
| Android Emulator + a system image | Latest | Android Studio → Virtual Device Manager |
| Node | 18+ | brew install node |
Wire JDK 17 into the system¶
brew install puts the binaries in place but doesn't make them the system default. Symlink it:
sudo ln -sfn /usr/local/opt/openjdk@17/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk-17.jdk
Verify:
java -version
# openjdk version "17.0.x"
Set environment variables¶
Add to ~/.zshrc (or ~/.bashrc):
# Java
export JAVA_HOME="/usr/local/opt/openjdk@17"
# Android SDK — default Mac install location
export ANDROID_HOME="$HOME/Library/Android/sdk"
# CLI tools on PATH
export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$PATH"
Reload your shell: source ~/.zshrc.
M2 Macs running Rosetta Homebrew
If brew --prefix openjdk@17 returns a /usr/local/... path rather than /opt/homebrew/..., your Homebrew is installed under Rosetta (Intel-compatible mode). This is fine — the paths above already match. If you migrated to a native Apple Silicon Homebrew, swap /usr/local for /opt/homebrew everywhere.
Create an emulator¶
Android Studio → Tools → Device Manager → Create Virtual Device.
| Setting | Pick |
|---|---|
| Device | Pixel 9 (or any reasonably recent phone profile) |
| System image | API 35 (Android 15) |
| Graphics | Hardware - GLES 2.0 |
Boot it once before running anything else — first boot can take 60+ seconds.
Android — running the app¶
From the repo root:
task dev:android # or: cd frontend && npx expo run:android
What this does:
- Checks for a connected Android device or running emulator. If none is found, prints an error.
- Compiles the native Android code (Gradle build, takes a few minutes the first time, faster on subsequent runs).
- Installs the APK on the device.
- Starts Metro and connects.
After the first install, daily work is task dev:frontend and press a in Metro to attach to the existing APK on the running emulator. Fast Refresh applies JS edits without rebuilding. Re-run task dev:android only after a native dep change or an app.json plugin update.
Pointing the app at your local backend¶
The Android emulator runs in its own VM — localhost from inside the emulator is the emulator itself, not your Mac. Use the loopback address 10.0.2.2 to reach services running on the host:
EXPO_PUBLIC_API_URL=http://10.0.2.2:8080/api/v1 npx expo run:android
A physical tethered device sees your Mac directly via its LAN IP (192.168.1.x etc.).
Choose a specific target¶
# List devices visible to ADB
adb devices
# Build for a specific device id
npx expo run:android --device <id>
# Build a release variant locally (signed with the debug keystore)
npx expo run:android --variant release
Reset Metro cache when things get weird¶
rm -rf $TMPDIR/metro-* $TMPDIR/haste-map-*
task dev:frontend:clear
Android — Google Maps integration¶
The map screens use the Google Maps SDK for Android, which is gated on a SHA-1 fingerprint registered in the Google Cloud Console. Without this, map tiles render as a gray grid.
Get the debug keystore SHA-1¶
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey \
-storepass android \
-keypass android \
2>/dev/null \
| grep 'SHA1:'
You'll get something like:
SHA1: 5E:8F:16:06:2E:A3:CD:2C:4A:0D:54:78:76:BA:A6:F3:8C:AB:F6:25
Register it in Google Cloud Console¶
- GCP Console → APIs & Services → Credentials (in project
development-485000). - Open the Maps SDK for Android API key.
- Application restrictions → Android apps → add an entry:
- Package name:
com.tomoda.app - SHA-1 fingerprint: paste the value above.
- Package name:
- Save. Map tiles should render on next app reload (allow a minute for propagation).
Each developer registers their own debug SHA-1. The production SHA-1 (used for Play-signed builds) is registered separately by whoever runs the release pipeline — see Play Store.
iOS — toolchain¶
| Tool | Required | How |
|---|---|---|
| Xcode | Latest stable | App Store |
| Xcode command-line tools | Bundled | xcode-select --install once after Xcode |
| CocoaPods | Latest | sudo gem install cocoapods |
| iOS Simulator runtime | Bundled with Xcode | Open Xcode once → Settings → Platforms → install a recent iOS version |
| Apple Developer Program | Recommended (paid) | https://developer.apple.com/programs/ |
xcode-select -p must print /Applications/Xcode.app/Contents/Developer. If it prints the bare CLI tools path, fix with:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
iOS — one-time per-developer setup¶
The frontend/ios/ folder is gitignored; each developer regenerates it locally via Expo prebuild. Per-developer state (signing team, certificates, provisioning profiles) lives in the local ios/Tomoda.xcodeproj and never touches the repo. Shared fixes (Podfile patches, plugin config) live in frontend/plugins/ and frontend/app.config.js and apply automatically on every prebuild.
1. Pick a signing team in Xcode¶
Required even for Simulator-only builds (Xcode 16+ refuses to build without a team).
open frontend/ios/Tomoda.xcworkspace
The Team selection is stored in the local (gitignored) project.pbxproj. Nothing to commit.
2. Install an Apple Development certificate¶
Xcode → Settings (⌘,) → Accounts → click your Apple ID → Manage Certificates… → click + in the bottom-left → Apple Development.
The cert + private key land in the login keychain. Verify with:
security find-identity -v -p codesigning
# expect: "Apple Development: Your Name (TEAMID)"
If you see 0 valid identities found, the private key didn't save. Open Keychain Access → login → Certificates → delete any key-less Apple Development entries, then redo the Manage Certificates → + step and click Always Allow on the keychain prompt.
iOS — running the app¶
Pick one based on where you want the binary:
| Target | Task | What it does |
|---|---|---|
| Simulator (default) | task dev:ios |
Boots an iPhone Simulator if none is up, runs expo run:ios for a clean build + install + Metro attach |
| Connected iPhone | task dev:ios:device |
Runs expo prebuild, opens the Xcode workspace, starts Metro with --dev-client. You press ⌘R in Xcode to build & install on the phone |
The Simulator wrapper verifies the toolchain (Xcode path, xcrun simctl, CocoaPods) before handing off. First build takes ~3–5 minutes either way; the dev binary is installed and Metro attaches.
After the first install on the Simulator, daily work is:
task dev:frontend # starts Metro
# press i → opens the already-installed dev binary
On a connected iPhone, the Xcode-based flow is the same as Simulator: task dev:frontend after the first task dev:ios:device and the app reconnects to Metro on its own (assuming the phone and Mac share a Wi-Fi network).
Fast Refresh applies JS edits in <1 second. Re-run task dev:ios (or task dev:ios:device) only when you:
- Add a native dependency (
npm install some-native-pkg) - Change
app.json/app.config.jsplugins or any native config - Bump Expo SDK or React Native
Fallback: build via Xcode directly¶
If task dev:ios fails at the xcodebuild step on a new iOS Simulator runtime (Expo CLI sometimes misclassifies brand-new runtimes as physical devices and demands provisioning), bypass it:
open frontend/ios/Tomoda.xcworkspace- Top toolbar → destination dropdown (between the scheme name and the play button) → pick a Simulator (e.g. iPhone 17 Pro)
- Press ⌘R
Xcode builds and installs the dev binary on the Simulator. From then on, task dev:frontend + i reattaches without rebuilding.
Choose a tethered iPhone instead¶
Two paths. Task path (recommended — handles prebuild + Metro + Xcode in one go):
task dev:ios:device
The task:
- Runs
npx expo prebuild --clean --platform iosso theios/project tree reflects currentapp.config.js+ plugins (new native deps land here). The clean regen is required because the Share Extension target (injected by@bacons/apple-targets) crashes on an in-place update, so the task always regeneratesios/from scratch. - Opens
ios/Tomoda.xcworkspacein Xcode. - Starts Metro with
--dev-clientin the same terminal.
In Xcode, pick your iPhone from the destination dropdown and press ⌘R to build & install. When the binary launches on the phone it connects to the Metro bundler that's already running in your terminal. JS-only changes after that just need a save; re-run task dev:ios:device only when a new native module gets added.
Direct expo path (skips Xcode UI, fine if your signing setup is already happy):
cd frontend && npx expo run:ios --device
Picks a connected device interactively.
Either path requires: the device registered to your team (Xcode → Window → Devices and Simulators → your iPhone → Use for Development), Developer Mode on the iPhone (Settings → Privacy & Security → Developer Mode), and the iPhone trusting this Mac ("Trust This Computer" tapped after the first plug-in).
Local backend from the Simulator¶
The iOS Simulator shares the host network, so http://127.0.0.1:8080 Just Works (no 10.0.2.2-style indirection like Android).
Reusing the dev client across sessions¶
The binary that task dev:android / task dev:ios installs is a dev client: once it's on your device/emulator you don't need to rebuild it for JS changes. Run task dev:frontend and connect — your JS bundle hot-reloads against the installed dev client's native runtime. Rebuild the binary (expo run:*) only when you change native code or add a native module.
See Native Testing for using dev clients in QA workflows.
Common pitfalls¶
iOS¶
| Symptom | Cause | Fix |
|---|---|---|
xcrun simctl help exited with non-zero code: 72 |
xcode-select points at bare CLI tools, not Xcode |
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer |
No code signing certificates are available to use |
No Apple Development cert in the login keychain |
Xcode → Settings → Accounts → Manage Certificates → + → Apple Development |
No profiles for 'com.tomoda.app' were found while building for a Simulator |
Expo CLI misclassified a new iOS Simulator runtime as a physical device | Use the Xcode fallback above: open Tomoda.xcworkspace, pick a Simulator destination, ⌘R |
simctl openurl ... exited with non-zero code: 60 |
Dev binary not installed; exp:// URL has no handler |
Run task dev:ios once to install the binary, then use task dev:frontend + i |
'react/utils/Telemetry.h' file not found |
RN / Pods version drift after an accidental npm i ...@latest rewrite of package.json |
git checkout frontend/package.json frontend/package-lock.json, then rm -rf frontend/{node_modules,ios} && npm install and retry task dev:ios |
Swift pod (AppCheckCore) refuses to integrate as static lib |
GoogleUtilities / RecaptchaInterop need modular headers |
Handled automatically by frontend/plugins/with-modular-headers.js on every prebuild; if you deleted the plugin, re-add it |
| Cert exists but Xcode still demands signing | Team on the target doesn't match the cert's team | Signing & Capabilities → set Team to the same (TEAMID) shown in security find-identity -v -p codesigning |
Android¶
- "No connected devices" — open Android Studio → Device Manager → Play your emulator manually before running
task dev:android. - "Task 'installDebug' not found" — you're not in the
frontend/folder.cd frontendfirst. - Network error on login — emulator can't reach
localhost. Use10.0.2.2for the API URL. - Map tiles render as gray — debug keystore SHA-1 not registered in Cloud Console. See the Google Maps section above.
- JDK mismatch on Gradle build — confirm
java -versionshows 17, not 11 or 21.
What this page does NOT cover¶
- The release pipeline — see Native Release for the EAS Build/Submit flow.
- Store portal management — see Play Store for Google Play Console, App Store for App Store Connect.
- Testing strategy — see Native Testing for emulator/device QA flows and OAuth caveats.