← Back to the journal

Android Setup

Emulators, adb, Gradle and the Android errors that read like they were written for someone else.

Android tooling fails in ways that don’t say what’s wrong. This is the symptom to cause list for the ones that come back every few months.

PATH: do it once, properly

# ~/.zshrc  (macOS default SDK location)
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin

On Linux the SDK is usually at $HOME/Android/Sdk (capital S). Some tools want ANDROID_HOME and others want ANDROID_SDK_ROOT, and the deprecation notices point in both directions, so set both and move on.

emulator: command not found

The tempting fix:

ln -s $ANDROID_HOME/tools/emulator /usr/local/bin/emulator   # don't

It gets you a working command, and then this, forever:

PANIC: Missing emulator engine program for 'x86' CPU

The emulator moved out of tools/ into its own emulator/ directory years ago, and it resolves its engine binaries relative to its own location, so a symlink from anywhere else can’t find them. Put $ANDROID_HOME/emulator on PATH instead, as above, and use the real binary.

emulator -list-avds                 # what you have
emulator -avd Pixel_7_API_34        # start one

Emulator options I keep forgetting

emulator -avd Pixel_7_API_34 -wipe-data          # factory reset a wedged AVD
emulator -avd Pixel_7_API_34 -no-snapshot-load   # cold boot, ignore saved state
emulator -avd Pixel_7_API_34 -no-audio -gpu host # faster, quieter
emulator -avd Pixel_7_API_34 -dns-server 8.8.8.8 # emulator has no internet

-no-snapshot-load is the fix for an emulator that boots straight into a frozen app. It’s restoring a snapshot taken mid-crash.

The emulator can’t reach your machine on localhost. From inside it, your host is 10.0.2.2, so a dev API on http://localhost:3000 is http://10.0.2.2:3000 to the app. On a physical device it’s your LAN IP, or adb reverse.

adb, and the bundler

adb devices                     # is anything even connected?
adb reverse tcp:8081 tcp:8081   # let the device reach Metro on your machine
adb -s emulator-5554 install app.apk   # pick a device when several are attached
adb logcat *:E                  # errors only
adb logcat -s ReactNativeJS     # just your JS console output

“Unable to load script. Make sure you’re either running Metro…” on a physical device is almost always the missing adb reverse. It has to be re-run after every reconnect and after adb kill-server.

device unauthorized means the RSA prompt on the phone was dismissed or the key is stale:

adb kill-server && adb start-server    # re-triggers the prompt on the device

INSTALL_FAILED_UPDATE_INCOMPATIBLE means a build signed with a different key, usually a release build, is already installed. Uninstall first:

adb uninstall com.yourapp

INSTALL_FAILED_INSUFFICIENT_STORAGE on an emulator that looks empty: wipe it. The AVD’s data partition is tiny by default.

Dev menu on a device with no shake gesture:

adb shell input keyevent 82

Metro

lsof -ti:8081 | xargs kill      # "port 8081 already in use", usually a dead run
npx react-native start --reset-cache

The cache reset is the answer to “I renamed a file and Metro still resolves the old one”, and to most Unable to resolve module errors where the module is plainly right there.

Gradle

cd android
./gradlew clean                 # after native dependency changes
./gradlew --stop                # kill daemons holding stale config
./gradlew assembleDebug --stacktrace   # the real error is below the summary

The wrong JDK is the top cause of unreadable Gradle failures. Current React Native expects JDK 17, and if a stray 21 or 11 is first on PATH you get Unsupported class file major version or a com.android.tools NoSuchMethodError.

/usr/libexec/java_home -V                        # list installed JDKs (macOS)
export JAVA_HOME=$(/usr/libexec/java_home -v 17) # pin it in ~/.zshrc

For Failed to install the following Android SDK packages ... not accepted:

sdkmanager --licenses           # accept everything, once

Builds that fail only in CI are usually memory. Give the daemon a real heap in android/gradle.properties.

org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m

When nothing makes sense

In order, cheapest first. Stop as soon as it builds:

npx react-native start --reset-cache
cd android && ./gradlew clean && cd ..
rm -rf android/.gradle android/app/build
rm -rf node_modules && npm ci
watchman watch-del-all           # if watchman is installed

Re-run pod install too if the project is a monorepo where iOS and Android share a lockfile. A partial npm ci will leave the native side stale.