Handling dependencies conflict and locking in React Native

UPDATE 08/2018 : This article is deprecated, you can now handle this very easily with native dependency locking with Gradle >= 4.8.


Developing React Native apps is fast and enjoyable because we rarely have to deal with the native part of the application thanks to all the native modules available. Sadly, sometimes we encounter issues very close to the iOS or Android native development side. Recently, we came across a module conflict and locking issue on Android.

The problem

The app we were working on is dependent on many native modules and specifically these two:

🗂 /app/android/app/build.gradle

dependencies {
    ...
    compile project(':react-native-firebase-analytics')
    compile project(':react-native-onesignal')

Which gives us, after trying to build… 💥

:app:transformClassesWithJarMergingForDebug FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:transformClassesWithJarMergingForDebug'.
> com.android.build.api.transform.TransformException: java.util.zip.ZipException: duplicate entry: com/google/android/gms/common/api/internal/zzcc.class

After some googling and browsing some stackoverflow issues, it seems that this is linked to a conflicting library, sadly not much more information is available about where to look.

Gradle has a nice command to display the dependency tree that might help us.

$ ./gradlew app:dependencies
+--- project :react-native-firebase-analytics
|    +--- com.facebook.react:react-native:+ -> 0.48.4 (*)
|    +--- com.google.firebase:firebase-core:+ -> 11.4.0
|    |    \--- com.google.firebase:firebase-analytics:11.4.0
|    |         +--- com.google.firebase:firebase-common:11.4.0
|    |         |    +--- com.google.android.gms:play-services-basement:11.4.0
...
|    \--- com.google.firebase:firebase-analytics:+ -> 11.4.0 (*)
+--- project :react-native-onesignal
|    +--- com.facebook.react:react-native:+ -> 0.48.4 (*)
|    +--- com.onesignal:OneSignal:3.+ -> 3.6.5
|    +--- com.google.android.gms:play-services-gcm:+ -> 11.0.2
|    |    +--- com.google.android.gms:play-services-base:11.0.2
|    |    |    +--- com.google.android.gms:play-services-basement:11.0.2 (*)

We can see that both react-native-firebase-analytics and react-native-onesignal depends on com.google.android.gms:play-services-basement but with different versions.

The solution

Via Gradle, we can exclude the conflicting module com.google.android.gm from both our dependencies like so and then ask for a specific version. This also prevents including too many libs from Google play services in our APK (less bundled code, yeah!).

dependencies {
    compile(project(':react-native-firebase-analytics')) {
        exclude group: 'com.google.android.gms'
    }
    compile(project(':react-native-onesignal')) {
        exclude group: 'com.google.android.gms'
    }
    compile 'com.google.android.gms:play-services-gcm:11.4.0'
    compile 'com.google.android.gms:play-services-basement:11.4.0'

Locking Android dependencies

After solving the conflict and trying to compile again a few weeks later, we got the same issue back without any native code modification of our app! 💥

This time the Gradle command showed us this:

+--- project :react-native-onesignal
|    +--- com.facebook.react:react-native:+ -> 0.48.4 (*)
|    \--- com.onesignal:OneSignal:3.+ -> 3.6.5
+--- project :react-native-firebase-analytics
|    +--- com.facebook.react:react-native:+ -> 0.48.4 (*)
|    +--- com.google.firebase:firebase-core:+ -> 11.6.0
|    |    \--- com.google.firebase:firebase-analytics:11.6.0
|    |         +--- com.google.firebase:firebase-common:11.6.0
|    |         |    +--- com.google.android.gms:play-services-basement:11.6.0
+--- com.google.android.gms:play-services-basement:11.4.0 -> 11.6.0 (*)

react-native-onesignal still got the dependency excluded but com.google.firebase:firebase-core now requires the 11.6.0 version. Since the dependency is not explicitly locked by the module, gradle resolves it with the latest version available.

Changing these two lines to require the latest version fixed the issue, but just until the next release of the firebase module…

    compile 'com.google.android.gms:play-services-gcm:11.4.0'
    compile 'com.google.android.gms:play-services-basement:11.4.0'

On a JavaScript project, we usually go with yarn or npm to manage dependencies. Thankfully, since yarn came out, we got a nice yarn.lock file to lock our dependencies and the npm equivalent package-lock.json. Gradle has no built-in system for this but Netflix solved it with a nice plugin.

To use this plugin, add this at the top of your android/app/build.gradle:

apply plugin: 'nebula.dependency-lock'

and this to android/build.gradle:

dependencies {
    classpath 'com.netflix.nebula:gradle-dependency-lock-plugin:4.+'

then the command cd android && ./gradlew generateLock saveLock will generate a nice android/app/dependencies.lock that you can add to git to be sure to always have the same dependencies on every installation.

Native knowledges are always nice to have while working on a React Native project, those tips will improve the stability of your project with strict reproducible builds. We are glad to only had this problem on Android since it seems way more complicated on iOS.

Resources

Nos formations sur le sujet

  • React Native

    Développez des applications mobiles natives et cross-platform pour iOS et Android grâce à React Native.

blog comments powered by Disqus