Troubleshooting React Native projects and AndroidX

25 October 2019

I recently worked on a legacy React Native app that had issues running on Android. Like many software projects there is never enough time nor enough engineers so the project was not tested for Android. When they finally got around to running the app on Android it was too painful. I was asked to fix it.

Before going over the lessons learned lets explain what AndroidX is and why it has caused the react native community so much pain? Google essentially fixed the naming conventions of many of its core libraries. That’s all AndroidX is. Within normal Android projects developed in Android Studio the upgrade path was easy. But not for RN.

In RN we do not typically compile the native dependencies. We instead rely on the dependencies to deliver a compiled file that can be linked by RN. So we have a hard division, RN apps 0.59 and below or RN apps 0.60 and above.

What the heck is jetifier?

Do you have a situation where you are on RN 0.60 or above, but one of your dependencies has not upgraded to AndroidX yet? Jetifier will search through your npm dependencies, detect old android imports and modernize them. If you are on RN 0.59 or below you do not need to run jetifier. If you are on RN 0.60 and above jetifier is run for you automatically.

Javascript project best practices

One of the first things I addressed was getting the project to build properly on the javascript side. For serious projects you should specify the nodejs version that is known to work. In package.json there is an engines field. You should review and pick an LTS version of nodejs you are committed to making the project run on. You may end up with something like

"engnes": {
  "node": "10.17.0"
},

While you are at it you can use asdf and specify the language runtimes required for your project. You can add a .tool-versions file that reads like

python 2.7.17
nodejs 10.17.0

Note: some javascript libraries depend on python2 being available to build correctly.

Once you have npm install running correctly be sure to add package-lock.json in version control. Yes it should be tracked as that is a set of dependencies that is known to work for the project.

Build issues

Now you try to build your app $ react-native run-android but you see errors like

> Task :react-native-gesture-handler:compileDebugJavaWithJavac

These types of errors seem to map one to one to entries in android/app/build.gradle in the dependencies group. You should specify the exact version of the dependency to make the build complete.

How do I know what a good config looks like?

If your project is heavily customized it may be hard to even know what a correct RN app looks like. For extreme cases you can compute a diff on the config files against the example app for your RN version. Luckily the RN team has provided example builds for every version of RN. E.g. RN 0.57 build.gradle file. These examples are accessible by picking the correct branch and opening the local-cli/templates folder. Note, these are the example apps that are generated by the cli when you initialize a new project.

Upgrading Gradle

To get the app building I specified the android sdk version. This in turn required using a version of gradle around 5.x (5.6.2 for us). Something that confused me a lot was the mismatching numbers between the gradle version and the android plugin for the very same gradle version. You want to set the plugin to version 3.5.0. The gradle syntax for the wrapper task also changed between these gradle versions. You end up with

     dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
             }
         }
     }
    project.configurations.all {

        resolutionStrategy.eachDependency { details ->

            if (details.requested.group == 'com.android.support'
                    && !details.requested.name.contains('multidex') ) {
                details.useVersion "27.1.0"
            }
        }
    }
 }

wrapper {
    gradleVersion = '5.6.2'
 }

You also need to upgrade the gradle version in android/gradle/wrapper/gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip

Building legacy apps

If your app is RN 0.59 and below be sure to use dependencies that do not use AndroidX. This means reading through the dependencies releases on github, e.g. https://github.com/react-native-community/async-storage/releases and looking for the last release that was published before June 2019 (or thereabouts, read the release notes for the first mention of AndroidX). You can quickly list the possible versions by running $ npm view <package name> versions.

You can also review dependencies generated by gradle. You will be shown output like

+--- project :react-native-paged-contacts                                                                              
|    \--- com.facebook.react:react-native:+ -> 0.57.7 (*)                                                              
+--- project :react-native-mail                                                                                        
|    \--- com.facebook.react:react-native:+ -> 0.57.7 (*)                                                              
+--- project :react-native-open-settings                                                                               
|    \--- com.facebook.react:react-native:+ -> 0.57.7 (*)                                                              
+--- project :react-native-vector-icons                                                                                
+--- com.android.support:appcompat-v7:28.0.0 -> 27.1.0 (*)                                                             
+--- com.facebook.react:react-native:+ -> 0.57.7 (*)                                                                   
+--- com.google.firebase:firebase-core:16.0.8                                                                          
|    \--- com.google.firebase:firebase-analytics:16.4.0                                                                
|         +--- com.google.android.gms:play-services-measurement:16.4.0                                                 
|         |    +--- com.google.android.gms:play-services-basement:16.2.0 -> 17.0.0                                     
|         |    |    +--- androidx.collection:collection:1.0.0                                                          
|         |    |    |    \--- androidx.annotation:annotation:1.0.0                                                     
|         |    |    +--- androidx.core:core:1.0.0                                                                      
|         |    |    |    +--- androidx.annotation:annotation:1.0.0                                                     
|         |    |    |    +--- androidx.collection:collection:1.0.0 (*)                                                 
|         |    |    |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0                                               
|         |    |    |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0                                           
|         |    |    |    |    |    \--- androidx.annotation:annotation:1.0.0                                           
|         |    |    |    |    +--- androidx.arch.core:core-common:2.0.0                                                
|         |    |    |    |    |    \--- androidx.annotation:annotation:1.0.0                                           
|         |    |    |    |    \--- androidx.annotation:annotation:1.0.0                                                
|         |    |    |    \--- androidx.versionedparcelable:versionedparcelable:1.0.0  

This region of gradle output lists some AndroidX dependencies being brought into the project. Following Top-Master’s stackoverflow answer, we see that firebase-core is pulling in some AndroidX dependencies. In this particular case we can search the firebase release notes for the string androidx and see an annoucement on June 17th that the library was migrated over to AndroidX. Our android/app/build.gradle file listed

    implementation 'com.google.firebase:firebase-core:16.0.9'

which we can see from the notes was released after June 17th. The fix was to downgrade this to 16.0.8.

    implementation 'com.google.firebase:firebase-core:16.0.8'

You will have to repeat a similar process for every dependency that is not building correctly for you.

react-native-firebase compatibility

This project was using react-native-firebase. For these projects be sure to consult the compatibility table. Mainly you’ll want the correct version of play services listed in your build.gradle file

// android/app/build.gradle
implementation "com.google.android.gms:play-services-base:16.1.0"

realm compatibility

This project was complaining about realm building. For this I ended up upgrading the dependency so it could build correctly with gradle 5.x. Similar process, I read through the releases and found the smallest version that would work with a modern gradle version.

Conclusion

If you have any tips I missed please share them. You can use the upgrade helper to know what to change. One final gist that helped on this project.

If you need help solving your business problems with software read how to hire me.



comments powered by Disqus