how to correct a runtime error with minifyEnabled true

181 views Asked by At

Android-studio 2023.1.1 Canary 10

My app works well, and I just wanted to optimize its size. So, I added

            minifyEnabled true
            shrinkResources true

to build.gradle

After a lot of trial and error to compile bundle and apk, I finally get there.

But now, it's at runtime that I have an error:

2023-11-04 18:27:45.843 AndroidRuntime   E  FATAL EXCEPTION: main
                                            Process: XX.XX.XX, PID: 21345
                                            java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.String java.lang.CharSequence.toString()' on a null object reference
                                                at java.text.Normalizer.normalize(Normalizer.java:167)
                                                at c4.e.v(SourceFile:1)
                                                at c4.e.a(SourceFile:1)
                                                at c4.d.compare(SourceFile:1)
                                                at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
                                                at java.util.TimSort.sort(TimSort.java:220)
                                                at java.util.Arrays.sort(Arrays.java:1492)
                                                at java.util.ArrayList.sort(ArrayList.java:1470)
                                                at java.util.Collections.sort(Collections.java:206)
                                                at c4.e.D(SourceFile:1)
                                                at b4.g.H(SourceFile:1)
                                                at XX.XX.XX.RangtActivity.C(SourceFile:1)
                                                at XX.XX.XX.services.e$a.b(SourceFile:1)
                                                at o5.g$b$a.f(SourceFile:1)
                                                at o5.g$b$a.d(SourceFile:1)
                                                at o5.h.run(SourceFile:1)
                                                at android.os.Handler.handleCallback(Handler.java:938)
                                                at android.os.Handler.dispatchMessage(Handler.java:99)
                                                at android.os.Looper.loop(Looper.java:223)
                                                at android.app.ActivityThread.main(ActivityThread.java:7656)
                                                at java.lang.reflect.Method.invoke(Native Method)
                                                at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
                                                at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

However, I have no problem working with minifyEnabled false. Otherwise I could try to correct what is wrong.

I don't even understand what part of my code is at fault.

Is my only solution not to try to reduce the size of the application?

You will have understood, I am not at all an expert.

build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdk 33

    signingConfigs {
        release {
            storeFile file(RELEASE_STORE_FILE)
            storePassword RELEASE_STORE_PASSWORD
            keyAlias RELEASE_KEY_ALIAS
            keyPassword RELEASE_KEY_PASSWORD
            // Optional, specify signing versions used
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }

    defaultConfig {
        applicationId "XX.XX.XX"
        minSdkVersion 22
        targetSdkVersion 33
        versionCode 251
        versionName '2.5.1'
        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
        vectorDrawables.useSupportLibrary = true
    }
    buildTypes {
        debug {
            buildConfigField "String", "VARIANT", "\"dev\""
            buildConfigField "boolean", "DEV", "true"
//            buildConfigField "int", "FOO", "52"
        }
        release {
            signingConfig signingConfigs.release
            buildConfigField "String", "VARIANT", "\"\""
            buildConfigField "boolean", "DEV", "false"
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            android.applicationVariants.all { variant ->
                def appName
                //Check if an applicationName property is supplied; if not use the name of the parent project.
                if (project.hasProperty("applicationName")) {
                    appName = applicationName
                } else {
                    appName = parent.name
                }
                variant.outputs.all { output ->
//                    outputFileName = "${appName}-${output.baseName}-${variant.versionName}.apk"
                    outputFileName = "${appName}-${variant.versionName}.apk"
//                def formattedDate = new Date().format('yyyyMMddHHmmss')
//                outputFileName = "${appName}-${output.baseName}-${variant.versionName}-${formattedDate}.apk"
                }
            }
        }
    }
    productFlavors {
        demo {
            applicationId "XX.XX.XXdemo"
//            dimension 'app'
            versionNameSuffix '-demo'
        }
        full {
//            dimension 'app'
        }
    }
    compileOptions {
        targetCompatibility = 1.8
        sourceCompatibility = 1.8
    }
    packagingOptions {
        resources {
            excludes += ['META-INF/LICENSE.md', 'META-INF/NOTICE.md']
        }
    }
    namespace 'XX.XX.XX'
    flavorDimensions += 'app'
}

dependencies {
    modules {
        module("org.jetbrains.kotlin:kotlin-stdlib-jdk7") {
            replacedBy("org.jetbrains.kotlin:kotlin-stdlib", "kotlin-stdlib-jdk7 is now part of kotlin-stdlib")
        }
        module("org.jetbrains.kotlin:kotlin-stdlib-jdk8") {
            replacedBy("org.jetbrains.kotlin:kotlin-stdlib", "kotlin-stdlib-jdk8 is now part of kotlin-stdlib")
        }
    }

    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
    implementation 'androidx.recyclerview:recyclerview:1.3.2'
    implementation 'androidx.preference:preference:1.2.1'
    implementation 'org.droidparts:droidparts-misc:3.2.5'
    implementation 'joda-time:joda-time:2.12.5'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'com.google.android.gms:play-services-vision:20.1.3'
    //noinspection GradleDependency
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.sun.mail:android-mail:1.6.7'
    implementation 'com.sun.mail:android-activation:1.6.7'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.annotation:annotation:1.7.0'
    implementation 'com.github.yalantis:ucrop:2.2.6'
    implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
}
allprojects {
   repositories {
      maven { url "https://jitpack.io" }
   }
}
configurations {
    configureEach {
        exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
    }
}

static String firstMatchingSubstring(String taskName, String[] keys) {
    def lcName = taskName.toLowerCase()
    for(String key: keys) { if(lcName.contains(key.toLowerCase())) return key }
    return null
}

/**
 *
 * @param taskName e.g., bundleMyFlavorRelease or bundleRelease
 * @return
 */
String getBuildType(String taskName) {
    return firstMatchingSubstring(taskName, getBuildTypeNames())
}

/**
 *
 * @param taskName e.g., bundleMyFlavorRelease
 * @return
 */
String getFlavor(String taskName) {
    return firstMatchingSubstring(taskName, getProductFlavorNames())
}

String[] getBuildTypeNames() {
    def types = []
    android.buildTypes.configureEach { type -> types.add(type.name) }
    return types
}

String[] getProductFlavorNames() {
    def flavors = []
    android.productFlavors.configureEach { flavor -> flavors.add(flavor.name) }
    return flavors
}

tasks.configureEach { task ->
    def name = task.name
    //Skip some unnecessary tasks
    if (name.startsWith("bundle")
            && !name.contains("Classes")
            && !name.contains("Resources")
            && name != "bundle") {

        def renameTaskName = "rename${task.name.capitalize()}Aab"
        def version = "${android.defaultConfig.versionName}"
        def flavor = getFlavor(name)
        def type = getBuildType(name)
        if(flavor == null || type == null) return

        def outputName
        if (flavor == "full")
            outputName = "DontEat-$version"
        else
            outputName = "DontEat-$version-$flavor"

        tasks.register(renameTaskName) {
            def path = "${rootDir}/app/${flavor}/${type}/"
            def originalFile = "$path/app-${flavor}-${type}.aab"

            doLast {
                if (file("$originalFile").exists()) {
                    ant.move file: "$originalFile",
                            tofile: "$path/${outputName}.aab"
                }
            }
        }
        task.finalizedBy(renameTaskName)
    }
}

proguard-rules.pro:

# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn org.joda.convert.**
-dontwarn org.joda.time.**
-keep class org.joda.time.** { *; }
-keep interface org.joda.time.** { *;}


# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod

# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations

# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
-keepattributes AnnotationDefault

# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
    @retrofit2.http.* <methods>;
}

# Ignore annotation used for build tooling.
#-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**

# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit

# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*

# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>

# Keep inherited services.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface * extends <1>

# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation

# R8 full mode strips generic signatures from return types if not kept.
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

# With R8 full mode generic signatures are stripped for classes that are not kept.
-keep,allowobfuscation,allowshrinking class retrofit2.Response

# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**

# A resource is loaded with a relative path so the package of this class must be preserved.
-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz

# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*

# OkHttp platform used only on JVM and when Conscrypt and other security providers are available.
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**

EDIT:

Thanks to the very valuable help of @sgjesse (*), I was finally able to find out more about the error:

java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.String java.lang.CharSequence.toString()' on a null object reference
                                             at java.text.Normalizer.normalize(Normalizer.java:167)

It's corrected now, but I still have problem running the app with R8. I'm trying to understand and correct them but it's off topic in this question.

(*) Adding -dontobfuscate to prodguard-rules.pro, downloading the app to my smartphone, connecting the smartphone to android-studio to see logcat

1

There are 1 answers

0
Sachin Singh On

You should have planned previously if you intended to use minifyEnabled and shrinkResources. What these two does is, they try to compress your apk size by some algorithm which somehow compresses your model class and other properties. And when those model class are compressed your app misbehaves, and you get exceptions here and there.

There are flags that should be used with your model class, which prevent them to be compressed. I don't know them exactly, but you can search them