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:
- Expo build process for native development:
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.
- Dumping useful info about an
apk
file:
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.- 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 aTODO
comment could be left to make it obvious that the implementation wasn’t complete.
- 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 whyNativeModule
was an empty object (the actual problem), I would have been found the route to an answer much quicker. - 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.
- 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.
One way to find the specific problem someone is struggling with is by ensuring you know: