React Native : Error: Duplicate resources - Android

I was trying to create a release apk file from Android but when I create a release apk with PNG image I'm getting Duplicate Resource error. Initially I thought this is happening because I made a mistake in the existing project but when I created a new project with a single Image component itself I'm getting the Duplicate Resource error. Here are the steps I followed

  1. Create a app - react-native init demo
  2. Create a assets folder in the project root folder.
  3. Add a PNG image inside the assets folder.
  4. Now implement the Image component with the above PNG image.
  5. Now bundle it using the cmd

    react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/

  6. Then generate release apk using Generate Signed APK from Android Studio.

This will throw the following error:

[drawable-mdpi-v4/assets_mario] /Users/jeffreyrajan/Tutorials/RN/errorCheck/android/app/src/main/res/drawable-mdpi/assets_mario.png [drawable-mdpi-v4/assets_mario] /Users/jeffreyrajan/Tutorials/RN/errorCheck/android/app/build/generated/res/react/release/drawable-mdpi-v4/assets_mario.png: Error: Duplicate resources
:app:mergeReleaseResources FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:mergeReleaseResources'.
> [drawable-mdpi-v4/assets_mario] /Users/jeffreyrajan/Tutorials/RN/errorCheck/android/app/src/main/res/drawable-mdpi/assets_mario.png   [drawable-mdpi-v4/assets_mario] /Users/jeffreyrajan/Tutorials/RN/errorCheck/android/app/build/generated/res/react/release/drawable-mdpi-v4/assets_mario.png: Error: Duplicate resources

* 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 22s

Note: When you generate a release apk without any PNG image you will not get any error, it will create you the release apk.

Here are the other files code.

App.js

import React, {Component} from 'react';
import {Platform, StyleSheet, Image, View} from 'react-native';

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Image source={require('./assets/mario.png')} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

package.json

{
  "name": "errorCheck",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest"
  },
  "dependencies": {
    "react": "16.6.0-alpha.8af6728",
    "react-native": "0.57.4"
  },
  "devDependencies": {
    "babel-jest": "23.6.0",
    "jest": "23.6.0",
    "metro-react-native-babel-preset": "0.49.0",
    "react-test-renderer": "16.6.0-alpha.8af6728"
  },
  "jest": {
    "preset": "react-native"
  }
}

Any solution for this?

Update:

Here are the other details

classpath 'com.android.tools.build:gradle:3.1.4'

ext {
        buildToolsVersion = "27.0.3"
        minSdkVersion = 16
        compileSdkVersion = 27
        targetSdkVersion = 26
        supportLibVersion = "27.1.1"
    }

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

Tried with Android Studio 3.0, 3.0.1, 3.1, 3.1.4 & 3.2


Solution 1:

(UPDATED)

This solution works for me

rm -rf ./android/app/src/main/res/drawable-*

rm -rf ./android/app/src/main/res/raw

In my case, build failed because there is a duplicated resources in my Android project (inside android folder), these two lines is necessary to remove duplicated resources, that's it.

Solution 2:

After trying a lot of solutions I found only Three solution is working. Here they are

Solution 1:

Clean the drawable folder from the terminal using Gradle. cd into the android folder, then run cmd ./gradlew clean

Solution 2:

After bundling delete the drawable folder from Android Studio. You could find this in android/app/src/main/res/drawable

Solution 3:

PLEASE DO NOT USE SOLUTION #2, AS PROPOSED BY THE ORIGINAL AUTHOR! All packages under node_modules are generated, and any changes you make will be lost when the react-native package is reinstalled / upgraded.

In this solution you no need to delete any drawable folder. Just add the following code in the react.gradle file which you could find under node_modules/react-native/react.gradle path

doLast {
    def moveFunc = { resSuffix ->
        File originalDir = file("$buildDir/generated/res/react/release/drawable-${resSuffix}");
        if (originalDir.exists()) {
            File destDir = file("$buildDir/../src/main/res/drawable-${resSuffix}");
            ant.move(file: originalDir, tofile: destDir);
        }
    }
    moveFunc.curry("ldpi").call()
    moveFunc.curry("mdpi").call()
    moveFunc.curry("hdpi").call()
    moveFunc.curry("xhdpi").call()
    moveFunc.curry("xxhdpi").call()
    moveFunc.curry("xxxhdpi").call()
}

For reference I will add the full react.gradle file code here

import org.apache.tools.ant.taskdefs.condition.Os

def config = project.hasProperty("react") ? project.react : [];

def cliPath = config.cliPath ?: "node_modules/react-native/local-cli/cli.js"
def bundleAssetName = config.bundleAssetName ?: "index.android.bundle"
def entryFile = config.entryFile ?: "index.android.js"
def bundleCommand = config.bundleCommand ?: "bundle"
def reactRoot = file(config.root ?: "../../")
def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"]
def bundleConfig = config.bundleConfig ? "${reactRoot}/${config.bundleConfig}" : null ;


afterEvaluate {
    android.applicationVariants.all { def variant ->
        // Create variant and target names
        def targetName = variant.name.capitalize()
        def targetPath = variant.dirName

        // React js bundle directories
        def jsBundleDir = file("$buildDir/generated/assets/react/${targetPath}")
        def resourcesDir = file("$buildDir/generated/res/react/${targetPath}")

        def jsBundleFile = file("$jsBundleDir/$bundleAssetName")

        // Additional node and packager commandline arguments
        def nodeExecutableAndArgs = config.nodeExecutableAndArgs ?: ["node"]
        def extraPackagerArgs = config.extraPackagerArgs ?: []

        def currentBundleTask = tasks.create(
            name: "bundle${targetName}JsAndAssets",
            type: Exec) {
            group = "react"
            description = "bundle JS and assets for ${targetName}."

            // Create dirs if they are not there (e.g. the "clean" task just ran)
            doFirst {
                jsBundleDir.deleteDir()
                jsBundleDir.mkdirs()
                resourcesDir.deleteDir()
                resourcesDir.mkdirs()
            }

            doLast {
                def moveFunc = { resSuffix ->
                    File originalDir = file("$buildDir/generated/res/react/release/drawable-${resSuffix}");
                    if (originalDir.exists()) {
                        File destDir = file("$buildDir/../src/main/res/drawable-${resSuffix}");
                        ant.move(file: originalDir, tofile: destDir);
                    }
                }
                moveFunc.curry("ldpi").call()
                moveFunc.curry("mdpi").call()
                moveFunc.curry("hdpi").call()
                moveFunc.curry("xhdpi").call()
                moveFunc.curry("xxhdpi").call()
                moveFunc.curry("xxxhdpi").call()
            }

            // Set up inputs and outputs so gradle can cache the result
            inputs.files fileTree(dir: reactRoot, excludes: inputExcludes)
            outputs.dir jsBundleDir
            outputs.dir resourcesDir

            // Set up the call to the react-native cli
            workingDir reactRoot

            // Set up dev mode
            def devEnabled = !(config."devDisabledIn${targetName}"
                || targetName.toLowerCase().contains("release"))

            def extraArgs = extraPackagerArgs;

            if (bundleConfig) {
                extraArgs = extraArgs.clone()
                extraArgs.add("--config");
                extraArgs.add(bundleConfig);
            }

            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                commandLine("cmd", "/c", *nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
                    "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, *extraArgs)
            } else {
                commandLine(*nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
                    "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, *extraArgs)
            }

            enabled config."bundleIn${targetName}" ||
                config."bundleIn${variant.buildType.name.capitalize()}" ?:
                targetName.toLowerCase().contains("release")
        }

        // Expose a minimal interface on the application variant and the task itself:
        variant.ext.bundleJsAndAssets = currentBundleTask
        currentBundleTask.ext.generatedResFolders = files(resourcesDir).builtBy(currentBundleTask)
        currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask)

        // registerGeneratedResFolders for Android plugin 3.x
        if (variant.respondsTo("registerGeneratedResFolders")) {
            variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders)
        } else {
            variant.registerResGeneratingTask(currentBundleTask)
        }
        variant.mergeResources.dependsOn(currentBundleTask)

        // packageApplication for Android plugin 3.x
        def packageTask = variant.hasProperty("packageApplication")
            ? variant.packageApplication
            : tasks.findByName("package${targetName}")

        def resourcesDirConfigValue = config."resourcesDir${targetName}"
        if (resourcesDirConfigValue) {
            def currentCopyResTask = tasks.create(
                name: "copy${targetName}BundledResources",
                type: Copy) {
                group = "react"
                description = "copy bundled resources into custom location for ${targetName}."

                from resourcesDir
                into file(resourcesDirConfigValue)

                dependsOn(currentBundleTask)

                enabled currentBundleTask.enabled


            }

            packageTask.dependsOn(currentCopyResTask)
        }

        def currentAssetsCopyTask = tasks.create(
            name: "copy${targetName}BundledJs",
            type: Copy) {
            group = "react"
            description = "copy bundled JS into ${targetName}."

            if (config."jsBundleDir${targetName}") {
                from jsBundleDir
                into file(config."jsBundleDir${targetName}")
            } else {
                into ("$buildDir/intermediates")
                into ("assets/${targetPath}") {
                    from jsBundleDir
                }

                // Workaround for Android Gradle Plugin 3.2+ new asset directory
                into ("merged_assets/${targetPath}/merge${targetName}Assets/out") {
                    from jsBundleDir
                }
            }

            // mergeAssets must run first, as it clears the intermediates directory
            dependsOn(variant.mergeAssets)

            enabled currentBundleTask.enabled
        }

        packageTask.dependsOn(currentAssetsCopyTask)
    }
}

Credit: ZeroCool00 mkchx

Solution 3:

For generating debug apk

"debug-build": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/ && cd android && ./gradlew assembleDebug && cd .."

For generating release apk

"release-build": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/build/intermediates/res/merged/release/ && rm -rf android/app/src/main/res/drawable-* && rm -rf android/app/src/main/res/raw/* && cd android && ./gradlew assembleRelease && cd .."

Works fine for my react-native >= 0.61.2 project by using the above code on my scripts section of package.json file.

Solution 4:

The accepted answer will work, however it does not take into account that modifying a node package means that if you update your change will be lost (as well as being against best-practices, you should extend the module somehow).

This is originally from React-native android release error: duplicate resource

  1. Create folder "fixAndroid" in the "android" folder of your project ({project-root}/android/fixAndroid).

  2. Create file android-gradle-fix in the "fixAndroid" folder of your project ({project-root}/android/fixAndroid/android-gradle-fix).

            doLast {
        def moveFunc = { resSuffix ->
            File originalDir = file("${resourcesDir}/drawable-${resSuffix}")
            if (originalDir.exists()) {
                File destDir = file("${resourcesDir}/drawable-${resSuffix}-v4")
                ant.move(file: originalDir, tofile: destDir)
            }
        }
        moveFunc.curry("ldpi").call()
        moveFunc.curry("mdpi").call()
        moveFunc.curry("hdpi").call()
        moveFunc.curry("xhdpi").call()
        moveFunc.curry("xxhdpi").call()
        moveFunc.curry("xxxhdpi").call()
    }
    
    // Set up inputs and outputs so gradle can cache the result
    
  3. Create file android-release-fix.js in the "fixAndroid" folder you created:

            const fs = require('fs')
    
        try {
                var curDir = __dirname
                var rootDir = process.cwd()
    
                var file = `${rootDir}/node_modules/react-native/react.gradle`
                var dataFix = fs.readFileSync(`${curDir}/android-gradle-fix`, 'utf8')
                var data = fs.readFileSync(file, 'utf8')
    
                var doLast = "doLast \{"
                if (data.indexOf(doLast) !== -1) {
                    throw "Already fixed."
                }
    
                var result = data.replace(/\/\/ Set up inputs and outputs so gradle can cache the result/g, dataFix);
                fs.writeFileSync(file, result, 'utf8')
                console.log('Android Gradle Fixed!')
            } catch (error) {
                console.error(error)
            }
    
  4. Add script to package.json scripts section:

    "postinstall": "node ./android/fixAndroid/android-release-fix.js"
    

This will find and insert content of “android-gradle-fix” file into node_modules/react-native/react.gradle.

  1. Run npm install from the root of your project.
  2. Run rm -rf android/app/src/main/res/drawable-* from the root of your project.

Now you can bundle the release with either React Native at the console or Android Studio:

React Native command line

  1. cd {project-root}/android
  2. ./gradlew/bundleRelease

Android Studio

  1. Open folder android in Android Studio and build project.
  2. Select Build/Generate signed APK to build release.