Debugging React Native Android NativeModules with expo

The Context

Working on a React Native Expo project which uses the EAS build tools for CI whilst also using a custom native module for Mapbox turn-by-turn navigation which is not supported by the Expo Go app out of the box.
Each time a native module changes (updating native Android/ iOS code), a new build must be created which includes the native changes. With this new build, the Expo Go development server can continue to push changes to the JS layer via over-the-air updates, which retains great the DevX of Expo Go.
We already had native turn-by-turn navigation working for iOS and we were returning to the project to deliver feature parity for Android.
Setting up my development environment, I ran into several issues and learnt more about the expo and adb commands.

The Problem

After downloading the latest development build with the most recent native code included, I ran the build on my android emulator (a Pixel 3a!).
I first tried running the current setup and found that the app crashed, with no reference to the native module which was working on iOS.
I first logged out NativeModules imported directly from "react-native":
console.log({ NativeModules }) // Output: { "NativeModules": {} }
I had been told that the Android native module was setup ready to go, and after running npx expo prebuild --platform android --clean to generate the native source code for the project I could see a native module being created called NavigationViewActivityStarter.java which was being added in the NavigationReactViewPackage.java as per my understanding of how to register custom native modules in React Native:
public class NavigationViewReactPackage implements ReactPackage { @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new NavigationViewActivityStarter(reactContext)); return modules; } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } }
This was confusing and as I was setting up my development environment for the first time, I assumed that I had downloaded an old version of the build or set up my environment incorrectly in a way which could not access the native modules.

The Solution

I called in a more senior dev with more Android experience than me to help debugging quickly.
Together we looked through the code and found that although NativeModules logs out as an empty object, the reference to it was still there! The interface between the native Android and JS layers had not been named consistently between Android and iOS. On iOS the module is declared as MapboxNavigation whereas on Android we still had a default name declaration which was picked up from the docs:
@Override public String getName() { return "NavigationViewActivityStarter"; }
As soon as we used NativeModules.NavigationViewActivityStarter React Native could now see the module, despite the fact that it logs out as an empty object ({}).
I later read this thread, which explains that once you start the dev-client in debug mode, the runtime switches to Chrome’s V8, rather that JavaScriptCore.

Learnings

During the debugging process, I learned a few useful things:
  1. Expo build process for native development:
    1. We have a /plugins directory which stores our native code.
      Running npx expo prebuild --platform android --clean combines the React Native source code and the plugins into the native source code (i.e. creates the /android directory).
      Running npx expo run:android builds the native project apk file from the native source code (i.e. creates the /android/app/build directory).
      You can then install the debug build onto your emulator with adb install android/app/build/outputs/apk/debug/app-debug.apk (you need adb and the usual Android dev tools).
      Next run the expo development server (as normal) - . ./.env && yarn expo start --dev-client (yarn dev for us) - which publishes over-the-air updates to any dev client.
      Open the emulator and the development build should be installed ready to follow the regular expo development process.
  1. Dumping useful info about an apk file:
    1. Run aapt dump badging <path-to-apk> - I had to run this from $ANDROID_HOME/build-tools/33.0.0/ where my build tools are stored.
  1. The importance of keeping contracts consistent between native platforms - this really confused me as to why NativeModules was returning an empty object and I wasted more time than necessary than if the naming was kept constant from when the module was initially developed. This is the drawback of committing code that doesn’t work to main - it can be submitted as a WIP branch, or a TODO comment could be left to make it obvious that the implementation wasn’t complete.
  1. This was another instance of the XY Problem - because NativeModules was empty, I assumed my setup was incorrect and asked people for help with my setup (asking people to solve a problem one-removed from the real issue). If instead, I had asked for help solving why NativeModule was an empty object (the actual problem), I would have been found the route to an answer much quicker.
    1. One way to find the specific problem someone is struggling with is by ensuring you know:
      1. What their broad goal is: what they’re trying to achieve with the feature so that you can come up with alternative approaches to reach the goal if necessary.
      1. What happened that they didn’t expect to happen (e.g. NativeModules logged as an empty object) when coding, so you know the exact thing that triggered the request for help and not an adjacent issue that they are trying to solve in order to solve the initial problem.