Skip to content

Instantly share code, notes, and snippets.

@gregfenton
Last active February 26, 2021 17:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gregfenton/fcc5b7c2d34563bc3f9e2852de9a2573 to your computer and use it in GitHub Desktop.
Save gregfenton/fcc5b7c2d34563bc3f9e2852de9a2573 to your computer and use it in GitHub Desktop.
EXPO: a description of ejecting an app from Expo Managed Workflow to run & debug it in Android Studio

Debugging a problem with my Expo app

TL;DR

See the Summary section.

Table of contents

Background

I have an Expo app, built with the Managed Workflow. It runs great on iOS and mostly fine on Android...except that the app has a tendency to red-screen in the Android emulator (and crash/restart on a physical device).

The stack trace indicates: com.facebook.react.bridge.ReadableNativeMap cannot be cast to java.lang.String

Googling around didn't help much. Speaking with Very Helpful folks on the Expo Slack server, it was recommended to try debugging this at the Java level. I know Java development!

  • (...enterprise Java development...)
  • (...with Eclipse...)

My app uses Google Firebase for Authentication, Firestore, and Cloud Storage.

The nature of the crashes was less-than-predictable. Sometimes it would crash within minutes of the app starting and me clicking around. Sometimes if I just started the app and logged in, then let the app sit...and sit...and sit... it might crash after an hour.

The one consistent thing was the stack trace. It was always the above exception message with the fuller stack trace being:

09-29 17:21:00.008   876   876 D KeyguardClockSwitch: Updating clock: 521
09-29 17:21:14.498   470   470 E netmgr  : Failed to open QEMU pipe 'qemud:network': Invalid argument
09-29 17:21:15.490   485   485 E wifi_forwarder: RemoteConnection failed to initialize: RemoteConnection failed to open pipe
09-29 17:21:30.779 19667  8549 W st.exp.exponen: Accessing hidden method Lcom/android/org/conscrypt/ConscryptEngineSocket;->setUseSessionTickets(Z)V (blacklist,core-platform-api, reflection, denied)
09-29 17:21:30.780 19667  8549 W st.exp.exponen: Accessing hidden method Lcom/android/org/conscrypt/ConscryptEngineSocket;->setHostname(Ljava/lang/String;)V (blacklist,core-platform-api, reflection, denied)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative: Exception in native call
09-29 17:21:30.951 19667  7454 E unknown:ReactNative: java.lang.ClassCastException: abi38_0_0.com.facebook.react.bridge.ReadableNativeMap cannot be cast to java.lang.String
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.ReadableNativeArray.getString(ReadableNativeArray.java:1)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.JavaMethodWrapper$5.extractArgument(JavaMethodWrapper.java:2)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.JavaMethodWrapper$5.extractArgument(JavaMethodWrapper.java:1)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:16)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:2)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at android.os.Handler.handleCallback(Handler.java:938)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at android.os.Handler.dispatchMessage(Handler.java:99)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:1)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at android.os.Looper.loop(Looper.java:223)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at abi38_0_0.com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:8)
09-29 17:21:30.951 19667  7454 E unknown:ReactNative:   at java.lang.Thread.run(Thread.java:923)
09-29 17:21:30.975 19667 19667 D ReactNative: CatalystInstanceImpl.destroy() start
09-29 17:21:30.975 19667 19667 D ReactNative: CatalystInstanceImpl.destroyV1() start
09-29 17:21:30.976 19667  7454 I ReactNative: [GESTURE HANDLER] Tearing down gesture handler registered for root view abi38_0_0.host.exp.exponent.ReactUnthemedRootView{7a989b8 V.E...... ........ 0,0-1440,2792 #f1}

Having avoided platform-specific coding (that's why I went with Expo/React-Native), I was at the mercy of others. But following the advice from the Slack channel, I was empowering myself! (damn...)

Here is my experience getting from an Expo Managed Workflow running blistfully-platform-unaware with simple commands like expo start, to a Bare Workflow running with a native platform debugger via Android Studio 😬.

Ejecting my project

Cedric from the Expo team pointed me to the starting docs for Expo Eject (the "bare workflow"). I followed the steps, but ran into problems (that I detail, and work out(!!), below):

  1. cd ~/work/

  2. cp -a rn_tick8s fred <<-- my project's name is 'rn_tick8s'

  3. cd fred <<-- folder we'll do the eject work in

  4. expo eject

    βœ— expo eject
    
    Your git working tree is clean
    To revert the changes after this command completes, you can run the following:
      git clean --force && git reset --hard
    
    πŸ“  Android package Learn more.
    
    ? What would you like your Android package name to be? com.modeldriventhinking.tick8s.roman_stage
                                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    πŸ“  iOS Bundle Identifier Learn more.
    
    ? What would you like your iOS bundle identifier to be? com.modeldriventhinking.tick8s.roman-stage
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    βœ” Created native projects
    βœ” Added Metro bundler configuration.
    βœ” Updated package.json and added index.js entry point for iOS and Android.
    
    🧢 Using Yarn to install packages. You can pass --npm to use npm instead.
    
    βœ” Cleaned JavaScript dependencies.
    βœ” Installed JavaScript dependencies.
    β Ή iOS config syncing
    Using node to generate images. This is much slower than using native packages.
    β€Ί Optionally you can stop the process and try again after successfully running `npm install -g sharp-cli`.
    
    ⚠️  iOS config synced with warnings that should be fixed:
    - splash: Unable to automatically configure splash screen. Please refer to the expo-splash-screen README for more information: https://github.com/expo/expo/tree/   master/packages/expo-splash-screen
    ⚠️  Android config synced with warnings that should be fixed:
    - splash: Unable to automatically configure splash screen. Please refer to the expo-splash-screen README for more information: https://github.com/expo/expo/tree/   master/packages/expo-splash-screen
    
    βœ” Installed pods and initialized Xcode workspace.
    
    ⚠️  Your app includes 3 packages that require additional setup in order to run:
    - expo-camera: https://github.com/expo/expo/tree/master/packages/expo-camera
    - expo-constants: Constants.manifest is not available in the bare workflow. You should replace it with Updates.manifest. Learn more.
    - expo-image-picker: https://github.com/expo/expo/tree/master/packages/expo-image-picker
    
    ➑️  Next steps
    - πŸ’‘ You may want to run npx @react-native-community/cli doctor to help install any tools that your app may need to run your native projects.
    - πŸ”‘ Download your Android keystore (if you're not sure if you need to, just run the command and see): expo fetch:android:keystore
    - πŸ“ The property assetBundlePatterns does not have the same effect in the bare workflow. Learn more.
    
    β˜‘οΈ  When you are ready to run your project
    To compile and run your project in development, execute one of the following commands:
    - yarn ios
    - yarn android
    - yarn web

Post-Eject "Additional Steps" From Output of expo eject

  1. Added the expo-camera settings to android/build.gradle

    • added
    maven {
            // expo-camera bundles a custom com.google.android:cameraview
            url "$rootDir/../node_modules/expo-camera/android/maven"
    }
    
  2. Added the expo-camera settings to android/app/src/main/AndroidManifest.xml

  3. Removed the reference to expo-constants from package.json (it wasn't being used, I had already replaced all of its instances with Updates.manifest from expo-updates)

  4. Added the <activity> entries for expo-image-picker to AndroidManifest.xml

    • added
    <activity
      android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
      android:theme="@style/Base.Theme.AppCompat">
    </activity>
    
  5. Run npx @react-native-community/cli doctor

    βœ— npx @react-native-community/cli doctor
    Common
     βœ“ Node.js
     βœ“ yarn
     βœ“ Watchman - Used for watching changes in the filesystem when in development mode
    
    Android
     βœ“ JDK
     βœ“ Android Studio - Required for building and installing your app on Android
     βœ“ Android SDK - Required for building and installing your app on Android
     βœ“ ANDROID_HOME
    
    iOS
     βœ“ Xcode - Required for building and installing your app on iOS
     βœ“ CocoaPods - Required for installing iOS dependencies
     ● ios-deploy - Required for installing your app on a physical device with the CLI
    
    Errors:   0
    Warnings: 1
    
    Usage
     β€Ί Press f to try to fix issues.
     β€Ί Press e to try to fix errors.
     β€Ί Press w to try to fix warnings.
     β€Ί Press Enter to exit.

    I pressed ENTER because I don't care (right now) about building for iOS, just trying to debug Android.

  6. Run expo fetch:android:keystore

    βœ— expo fetch:android:keystore
    Accessing credentials for greg.fenton in project ********
    
    A file already exists at "/Users/greg/nosync/fred/********.jks"
      Renaming the existing file to OLD_1_tick8s-roman-stage.jks
    
    Saving Keystore to /Users/greg/nosync/fred/tick8s-roman-stage.jks
    Keystore credentials
      Keystore password: 9******************************c
      Key alias:         Q****************************************Q==
      Key password:      7******************************0
    
      Path to Keystore:  /Users/greg/nosync/fred/********.jks
    
  7. FYI: not sure what to do about assetBundlePatterns in app.json. Current value is:

    "assetBundlePatterns": [
       "**/*"
     ],

    so am leaving it as-is for now.

Let's get compiling

  1. yarn android

     βœ— yarn android
     yarn run v1.22.4
     $ react-native run-android
     info Running jetifier to migrate libraries to AndroidX. You can disable it using "--no-jetifier" flag.
     Jetifier found 1793 file(s) to forward-jetify. Using 12 workers...
     info Starting JS server...
     info Launching emulator...
     info Successfully launched emulator.
     info Installing the app...
     Starting a Gradle Daemon (subsequent builds will be faster)
     java.lang.NoClassDefFoundError: Could not initialize class org.codehaus.groovy.vmplugin.v7.Java7
       at org.codehaus.groovy.vmplugin.VMPluginFactory.<clinit>(VMPluginFactory.java:43)
       at org.codehaus.groovy.reflection.GroovyClassValueFactory.<clinit>(GroovyClassValueFactory.java:35)
       at org.codehaus.groovy.reflection.ClassInfo.<clinit>(ClassInfo.java:107)
       at org.codehaus.groovy.reflection.ReflectionCache.getCachedClass(ReflectionCache.java:95)
           //
           // ...
           //
       at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
       at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
       at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
       at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
       at java.base/java.lang.Thread.run(Thread.java:832)
    
     FAILURE: Build failed with an exception.
    
     * What went wrong:
     Could not initialize class org.codehaus.groovy.reflection.ReflectionCache
    
     * Try:
     Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
    
     * Get more help at https://help.gradle.org
    
     BUILD FAILED in 2s
    
     error Failed to install the app. Make sure you have the Android development environment set up: https://reactnative.dev/docs/environment-setup. Run CLI with --verbose flag for more details.
     Error: Command failed: ./gradlew app:installDebug -PreactNativeDevServerPort=8081
     java.lang.NoClassDefFoundError: Could not initialize class org.codehaus.groovy.vmplugin.v7.Java7
     error Command failed with exit code 1.
     info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
    • more input from @bycedric on Slack (thanks Cedric!) - dev environment requirements are available from the React Native team: https://reactnative.dev/docs/environment-setup
      • NOTE: on that page, you want to click the correct "tabs" (not necessarily intuitive):
        1. React Native CLI Quickstart (not Expo CLI Quickstart)
        2. macOS (or Windows, or Linux)
        3. Android (I'm trying to debug Android; don't care about iOS at this time)
    • I followed the instructions above.
      • Found that I had Android SDK 9 installed, but not SDK 10 (Q).
    • Ensure to install the correct SDK, and SDK Tools (all as per the instructions)
    • Ensure you update your shell (bash, zsh, etc.) correctly, and that the shell you are running in has the enviroment variables (ANDROID_HOME and PATH) set correctly.
  2. In the shell running the Metro server (started with the previous yarn android command), press ctrl-c to exit the server and press ENTER to close the terminal window

  3. yarn android

     $ yarn android
     yarn run v1.22.4
     $ react-native run-android
     info Running jetifier to migrate libraries to AndroidX. You can disable it using "--no-jetifier" flag.
     Jetifier found 1793 file(s) to forward-jetify. Using 12 workers...
     info Starting JS server...
     info Installing the app...
    
     FAILURE: Build failed with an exception.
    
     * What went wrong:
     Could not initialize class org.codehaus.groovy.runtime.InvokerHelper
    
     * Try:
     Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
    
     * Get more help at https://help.gradle.org
    
     BUILD FAILED in 522ms
    
     error Failed to install the app. Make sure you have the Android development environment set up: https://reactnative.dev/docs/environment-setup. Run CLI with --verbose flag for more details.
     Error: Command failed: ./gradlew app:installDebug -PreactNativeDevServerPort=8081
    
     error Command failed with exit code 1.
     info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

    Googling I found these (the SO article points to the GH issue):

    1. Edit android > gradle / wrapper > gradle-wrapper.properties
      1. update distributionUrl to distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
  4. In the shell running the Metro server (started with the previous yarn android command), press ctrl-c to exit the server and press ENTER to close the terminal window

  5. Run yarn android (third time is the charm! πŸ€) (it is building!!!!! . . . . . .)

  6. Metro server is up. App splash screen (the default one, not my custom one...will fix later) loads. But then Metro has stack trace:

    [Thu Oct 01 2020 14:53:20.834]  BUNDLE  ./index.js
    
    error: Error: Unable to resolve module `./App` from `index.js`:
    
    None of these files exist:
      * App(.native|.android.js|.native.js|.js|.android.json|.native.json|.json|.android.ts|.native.ts|.ts|.android.tsx|.native.tsx|.tsx)
      * App/index(.native|.android.js|.native.js|.js|.android.json|.native.json|.json|.android.ts|.native.ts|.ts|.android.tsx|.native.tsx|.tsx)
        at ModuleResolver.resolveDependency (/Users/greg/nosync/fred/node_modules/metro/src/node-haste/DependencyGraph/ModuleResolution.js:163:15)
        at ResolutionRequest.resolveDependency (/Users/greg/nosync/fred/node_modules/metro/src/node-haste/DependencyGraph/ResolutionRequest.js:52:18)
        at DependencyGraph.resolveDependency (/Users/greg/nosync/fred/node_modules/metro/src/node-haste/DependencyGraph.js:287:16)
        at Object.resolve (/Users/greg/nosync/fred/node_modules/metro/src/lib/transformHelpers.js:267:42)
        at /Users/greg/nosync/fred/node_modules/metro/src/DeltaBundler/traverseDependencies.js:434:31
        at Array.map (<anonymous>)
        at resolveDependencies (/Users/greg/nosync/fred/node_modules/metro/src/DeltaBundler/traverseDependencies.js:431:18)
        at /Users/greg/nosync/fred/node_modules/metro/src/DeltaBundler/traverseDependencies.js:275:33
        at Generator.next (<anonymous>)
        at asyncGeneratorStep (/Users/greg/nosync/fred/node_modules/metro/src/DeltaBundler/traverseDependencies.js:87:24)
  7. in my <PROJECT_ROOT>/package.json I have:

      "main": "src/app/layout/App.js",
  8. in my file <PROJECT_ROOT>/index.js I have:

    import {registerRootComponent} from 'expo';
    import App from './App';
    
    // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
    // It also ensures that whether you load the app in the Expo client or in a native build,
    // the environment is set up appropriately
    registerRootComponent(App);

    Though this worked under an Expo Managed Workflow, it isn't working in this Bare Workflow.

  9. edit line #2 of <PROJECT_ROOT>/index.js:

    import {registerRootComponent} from 'expo';
    import App from './src/app/layout/App';
    
    // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
    // It also ensures that whether you load the app in the Expo client or in a native build,
    // the environment is set up appropriately
    registerRootComponent(App);
  10. If you kept Metro running, in its window press r to reload the JS code. If you had shut it down, then run yarn android.

    • WE HAVE GREAT SUCCESS!
    • app running in emulator!
    • log in, data shows. GREAT SUCCESS! (/insert happy dance here)
  11. Shut down Metro with ctrl-c

Debugging in Android Studio

  1. Start Android Studio
  2. In its startup dialog, choose Open Existing Project.
    • browse to <PROJECT_ROOT>/android and click the Open button
  3. (~ 2 minutes) Wait....wait a while. Studio opened for me showing just a couple of nodes in the Project (left-hand panel). But it was busy in the background building and inspecting the project. Let it do its thing. Eventually it finished and it was clearly aware of the project, the emulator. Lots of nodes listed in the project It was ready to run!
  4. I enabled all Java breakpoints: Run Β» View Breakpoints... Β» Java Exception Breakpoints Β» Any exception then enable Enabled and Suspend (Thread). For my purposes, I also enabled Class filters and added two classes I am interested in:
    • com.facebook.react.bridge.ReadableNativeMap
    • com.facebook.react.bridge.ReadableNativeArray
  5. the app apparently still needs to connect to Metro to connect to. So I started it with yarn start.
  6. I ran my app for a while (approx 10 minutes), and then Android Studio caught the exception(!!!)
    • ends up that the object attempted to convert to a string was:
      {
        "NativeMap": {
          "toString": null,
          "b": {
            "toString": null,
            "set": null,
            "C": null,
            "K": null,
            "add": null,
            "f": false,
            "c": null,
            "b": 7,
            "a": {
              "forEach": null,
              "set": null,
              "get": null,
              "K": null,
              "C": null,
              "c": 7,
              "a": ["database", "VER", "gsessionid", "SID", "RID", "TYPE", "zx"],
              "b": {
                "zx": ["8**********8"],
                "TYPE": ["terminate"],
                "RID": [5*****7],
                "SID": ["8********************w"],
                "gsessionid": ["v******************************I"],
                "VER": [8],
                "database": ["projects/<MY_PROJECT_ID>/databases/(default)"]
              }
            },
            "get": null,
            "forEach": null
          },
          "a": false,
          "i": "",
          "g": "/google.firestore.v1.Firestore/Write/channel",
          "h": null,
          "c": "firestore.googleapis.com",
          "j": "",
          "f": "https"
        }
      }
      
      and having that data confirmed that these crashes are likely the result of expo issue #7371, which itself references this comment on expo/browser-polyfill issue #35 that shows the exact same data structure as the JSON string above.

Summary

Essentially, it looks like expo/browser-polyfill changes the React Native environment such that Firebase believes it is running in a web browser. And with that change, I think, what is happening is that Firebase mimics fetching an image as a form of "keep-alive" (or "session ping"). But the "browser" doesn't really behave like one and unexpected interruptions cause React Native's bridge to get "bad data" (...I'm sure I could dig for more specifics, but I have what I need for now: ditch expo-pixi/expo/browser-polyfill since I am not giving up on Firebase).

My app had been running on Android fine for quite some time. I have been doing a lot of tweaks to existing functionality in the past month - mainly UI tweaks and styling consistency and some code cleanup. I noticed the crashing a couple of weeks ago in development but ignored them to get a new beta-release out (and things were running fine on iOS). The one major feature that I did add in the past few weeks (why didn't it occur to me earlier??) was Signature Capture using expo-pixi.

For more reading on specifics, see:

I captured all of the above steps to detail out to:

  1. expo eject a managed workflow project into a bare workflow
  2. configure a Android Studio development environment
  3. run the bare workflow from the command line
  4. run the bare workflow from Android Studio
  5. debug the bare workflow from Android Studio, capturing exceptions without terminating the app so that you can inspect the conditions around the exception

Conclusion: At this time (expo SDK 38 & SDK 39, Firebase v7.9.0) expo-pixi (or, more precisely, expo/browser-polyfill) and the Firebase SDK do not play well together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment