Gradle : how to use BuildConfig in an android-library with a flag that gets set in an app
Asked Answered
B

7

69

My (gradle 1.10 and gradle plugin 0.8)-based android project consists of a big android-library that is a dependency for 3 different android-apps

In my library, I would love to be able to use a structure like this

if (BuildConfig.SOME_FLAG) {
    callToBigLibraries()
}

as proguard would be able to reduce the size of the produced apk, based on the final value of SOME_FLAG

But I can't figure how to do it with gradle as :

* the BuildConfig produced by the library doesn't have the same package name than the app
* I have to import the BuildConfig with the library package in the library
* The apk of an apps includes the BuildConfig with the package of the app but not the one with the package of the library.

I tried without success to play with BuildTypes and stuff like

release {
    // packageNameSuffix "library"
    buildConfigField "boolean", "SOME_FLAG", "true"
}
debug {
    //packageNameSuffix "library"
    buildConfigField "boolean", "SOME_FLAG", "true"
}

What is the right way to builds a shared BuildConfig for my library and my apps whose flags will be overridden at build in the apps?

Bolection answered 26/1, 2014 at 16:17 Comment(0)
E
22

You can't do what you want, because BuildConfig.SOME_FLAG isn't going to get propagated properly to your library; build types themselves aren't propagated to libraries -- they're always built as RELEASE. This is bug https://code.google.com/p/android/issues/detail?id=52962

To work around it: if you have control over all of the library modules, you could make sure that all the code touched by callToBigLibraries() is in classes and packages that you can cleave off cleanly with ProGuard, then use reflection so that you can access them if they exist and degrade gracefully if they don't. You're essentially doing the same thing, but you're making the check at runtime instead of compile time, and it's a little harder.

Let me know if you're having trouble figuring out how to do this; I could provide a sample if you need it.

Err answered 29/1, 2014 at 0:25 Comment(2)
I see. Thanks for the answer : I'll monitor the linked issue and revisit the question once something has been done about it. For the time being, to work around the issue, I did something a bit like your suggestion and the code I didn't want got indeed cleaved of.Bolection
You can just override the build variant of a LIbrary with ` defaultPublishConfig "release"` or defaultPublishConfig "debug"Instate
B
50

As a workaround, you can use this method, which uses reflection to get the field value from the app (not the library):

/**
 * Gets a field from the project's BuildConfig. This is useful when, for example, flavors
 * are used at the project level to set custom fields.
 * @param context       Used to find the correct file
 * @param fieldName     The name of the field-to-access
 * @return              The value of the field, or {@code null} if the field is not found.
 */
public static Object getBuildConfigValue(Context context, String fieldName) {
    try {
        Class<?> clazz = Class.forName(context.getPackageName() + ".BuildConfig");
        Field field = clazz.getField(fieldName);
        return field.get(null);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return null;
}

To get the DEBUG field, for example, just call this from your Activity:

boolean debug = (Boolean) getBuildConfigValue(this, "DEBUG");

I have also shared this solution on the AOSP Issue Tracker.

Buyers answered 12/8, 2014 at 14:34 Comment(11)
Is BuildConfig guaranteed to be in that package?Harrison
@Harrison I'd imagine there is a way to change where the BuildConfig is output - however from my experience this is guaranteed for the default setup.Buyers
This should be the accepted answer because it works!Instate
This seems to work fine until your applicationId differs from the packagename of your Java classes. (For example, if you are building variants as mentioned here: tools.android.com/tech-docs/new-build-system/…). It seems that context.getPackageName() will return the applicationId, which now may not be the package of your Java classes that BuildConfig.class resides in. Any work around for that?Incunabulum
@Incunabulum you could specify a class in your main flavor that has the default method, then in your other flavor directories, override this class and instead of using context.getPackageName(), you can specify the exact String.Buyers
@Incunabulum you might find my answer useful https://mcmap.net/q/263415/-gradle-how-to-use-buildconfig-in-an-android-library-with-a-flag-that-gets-set-in-an-appMaples
I recently discover a ClassNotFoundException with this method: If you declare a a suffix in your gradle's buildConfig (for example, if you want the same app but with different package name, depending on the debug or release build, for test), the context.getPackageName()will return the package name with the suffix on it, but the BuildConfig still will be deployed on the packageName without the suffix. So, the method will be looking for the com.example.app.suffix.BuildConfigclass, when the class is com.example.app.BuildConfig. Any ideas for this?Sansculotte
This method was promising, but as a library developer, it breaks once the app developer changes the applicationId of a flavor. There is still no solution to this problem.Voltaic
downvote as Class.getPackageName() is class loader dependend name - could return you not exactly what you would expect to :) i cant do a second down vote for usage Class.forName() without specifying class loader, third downvote for reflection access without assign accessibility for fieldGutter
Breaks when ProGuard kicks in!Tiruchirapalli
@Tiruchirapalli you would need to ensure the BuildConfig is not obfuscatedBuyers
C
36

Update: With newer versions of the Android Gradle plugin publishNonDefault is deprecated and has no effect anymore. All variants are now published.

The following solution/workaround works for me. It was posted by some guy in the google issue tracker:

Try setting publishNonDefault to true in the library project:

android {
    ...
    publishNonDefault true
    ...
}

And add the following dependencies to the app project that is using the library:

dependencies {
    releaseCompile project(path: ':library', configuration: 'release')
    debugCompile project(path: ':library', configuration: 'debug')
}

This way, the project that uses the library includes the correct build type of the library.

Copolymer answered 16/7, 2015 at 14:46 Comment(0)
E
22

You can't do what you want, because BuildConfig.SOME_FLAG isn't going to get propagated properly to your library; build types themselves aren't propagated to libraries -- they're always built as RELEASE. This is bug https://code.google.com/p/android/issues/detail?id=52962

To work around it: if you have control over all of the library modules, you could make sure that all the code touched by callToBigLibraries() is in classes and packages that you can cleave off cleanly with ProGuard, then use reflection so that you can access them if they exist and degrade gracefully if they don't. You're essentially doing the same thing, but you're making the check at runtime instead of compile time, and it's a little harder.

Let me know if you're having trouble figuring out how to do this; I could provide a sample if you need it.

Err answered 29/1, 2014 at 0:25 Comment(2)
I see. Thanks for the answer : I'll monitor the linked issue and revisit the question once something has been done about it. For the time being, to work around the issue, I did something a bit like your suggestion and the code I didn't want got indeed cleaved of.Bolection
You can just override the build variant of a LIbrary with ` defaultPublishConfig "release"` or defaultPublishConfig "debug"Instate
I
14

I use a static BuildConfigHelper class in both the app and the library, so that I can have the packages BuildConfig set as final static variables in my library.

In the application, place a class like this:

package com.yourbase;

import com.your.application.BuildConfig;

public final class BuildConfigHelper {

    public static final boolean DEBUG = BuildConfig.DEBUG;
    public static final String APPLICATION_ID = BuildConfig.APPLICATION_ID;
    public static final String BUILD_TYPE = BuildConfig.BUILD_TYPE;
    public static final String FLAVOR = BuildConfig.FLAVOR;
    public static final int VERSION_CODE = BuildConfig.VERSION_CODE;
    public static final String VERSION_NAME = BuildConfig.VERSION_NAME;

}

And in the library:

package com.your.library;

import android.support.annotation.Nullable;

import java.lang.reflect.Field;

public class BuildConfigHelper {

    private static final String BUILD_CONFIG = "com.yourbase.BuildConfigHelper";

    public static final boolean DEBUG = getDebug();
    public static final String APPLICATION_ID = (String) getBuildConfigValue("APPLICATION_ID");
    public static final String BUILD_TYPE = (String) getBuildConfigValue("BUILD_TYPE");
    public static final String FLAVOR = (String) getBuildConfigValue("FLAVOR");
    public static final int VERSION_CODE = getVersionCode();
    public static final String VERSION_NAME = (String) getBuildConfigValue("VERSION_NAME");

    private static boolean getDebug() {
        Object o = getBuildConfigValue("DEBUG");
        if (o != null && o instanceof Boolean) {
            return (Boolean) o;
        } else {
            return false;
        }
    }

    private static int getVersionCode() {
        Object o = getBuildConfigValue("VERSION_CODE");
        if (o != null && o instanceof Integer) {
            return (Integer) o;
        } else {
            return Integer.MIN_VALUE;
        }
    }

    @Nullable
    private static Object getBuildConfigValue(String fieldName) {
        try {
            Class c = Class.forName(BUILD_CONFIG);
            Field f = c.getDeclaredField(fieldName);
            f.setAccessible(true);
            return f.get(null);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

Then, anywhere in your library where you want to check BuildConfig.DEBUG, you can check BuildConfigHelper.DEBUG and access it from anywhere without a context, and the same for the other properties. I did it this way so that the library will work with all my applications, without needing to pass a context in or set the package name some other way, and the application class only needs the import line changed to suit when adding it into a new application

Edit: I'd just like to reiterate, that this is the easiest (and only one listed here) way to get the values to be assigned to final static variables in the library from all of your applications without needing a context or hard coding the package name somewhere, which is almost as good as having the values in the default library BuildConfig anyway, for the minimal upkeep of changing that import line in each application.

Inlet answered 2/5, 2015 at 7:41 Comment(3)
An fix class name reference ( constant BUILD_CONFIG ) within an library, does not seams to be a reliable solution for this issue.Edythedythe
The helpers class name never changes, hence a fixed class name reference is fine, and kind of the entire point of the helper. Its job is to grab the values from the application BuildConfig and store them in a class the library knows the name of.Melody
@KaneO'Riley Thank you sir it's a great solution. Much helps.Resolute
M
5

For the case where the applicationId is not the same as the package (i.e. multiple applicationIds per project) AND you want to access from a library project:

Use Gradle to store the base package in resources.

In main/AndroidManifest.xml:

android {
    applicationId "com.company.myappbase"
    // note: using ${applicationId} here will be exactly as above
    // and so NOT necessarily the applicationId of the generated APK
    resValue "string", "build_config_package", "${applicationId}"
}

In Java:

public static boolean getDebug(Context context) {
    Object obj = getBuildConfigValue("DEBUG", context);
    if (obj instanceof Boolean) {
        return (Boolean) o;
    } else {
        return false;
    }
}

private static Object getBuildConfigValue(String fieldName, Context context) {
    int resId = context.getResources().getIdentifier("build_config_package", "string", context.getPackageName());
    // try/catch blah blah
    Class<?> clazz = Class.forName(context.getString(resId) + ".BuildConfig");
    Field field = clazz.getField(fieldName);
    return field.get(null);
}
Maples answered 23/7, 2015 at 12:47 Comment(0)
W
1

use both

my build.gradle
// ...
productFlavors {
    internal {
        // applicationId "com.elevensein.sein.internal"
        applicationIdSuffix ".internal"
        resValue "string", "build_config_package", "com.elevensein.sein"
    }

    production {
        applicationId "com.elevensein.sein"
    }
}

I want to call like below

Boolean isDebug = (Boolean) BuildConfigUtils.getBuildConfigValue(context, "DEBUG");

BuildConfigUtils.java

public class BuildConfigUtils
{

    public static Object getBuildConfigValue (Context context, String fieldName)
    {
        Class<?> buildConfigClass = resolveBuildConfigClass(context);
        return getStaticFieldValue(buildConfigClass, fieldName);
    }

    public static Class<?> resolveBuildConfigClass (Context context)
    {
        int resId = context.getResources().getIdentifier("build_config_package",
                                                         "string",
                                                         context.getPackageName());
        if (resId != 0)
        {
            // defined in build.gradle
            return loadClass(context.getString(resId) + ".BuildConfig");
        }

        // not defined in build.gradle
        // try packageName + ".BuildConfig"
        return loadClass(context.getPackageName() + ".BuildConfig");

    }

    private static Class<?> loadClass (String className)
    {
        Log.i("BuildConfigUtils", "try class load : " + className);
        try { 
            return Class.forName(className); 
        } catch (ClassNotFoundException e) { 
            e.printStackTrace(); 
        }

        return null;
    }

    private static Object getStaticFieldValue (Class<?> clazz, String fieldName)
    {
        try { return clazz.getField(fieldName).get(null); }
        catch (NoSuchFieldException e) { e.printStackTrace(); }
        catch (IllegalAccessException e) { e.printStackTrace(); }
        return null;
    }
}
Waylan answered 3/2, 2016 at 16:27 Comment(0)
G
-3

For me this is the ONLY ONE AND ACCEPTABLE* SOLUTION TO determine the ANDROID APPLICATION BuildConfig.class:

// base entry point 
// abstract application 
// which defines the method to obtain the desired class 
// the definition of the application is contained in the library 
// that wants to access the method or in a superior library package
public abstract class BasApp extends android.app.Application {

    /*
     * GET BUILD CONFIG CLASS 
     */
    protected Class<?> getAppBuildConfigClass();

    // HELPER METHOD TO CAST CONTEXT TO BASE APP
    public static BaseApp getAs(android.content.Context context) {
        BaseApp as = getAs(context, BaseApp.class);
        return as;
    }

    // HELPER METHOD TO CAST CONTEXT TO SPECIFIC BASEpp INHERITED CLASS TYPE 
    public static <I extends BaseApp> I getAs(android.content.Context context, Class<I> forCLass) {
        android.content.Context applicationContext = context != null ?context.getApplicationContext() : null;
        return applicationContext != null && forCLass != null && forCLass.isAssignableFrom(applicationContext.getClass())
            ? (I) applicationContext
            : null;
    }
     
    // STATIC HELPER TO GET BUILD CONFIG CLASS 
    public static Class<?> getAppBuildConfigClass(android.content.Context context) {
        BaseApp as = getAs(context);
        Class buildConfigClass = as != null
            ? as.getAppBuildConfigClass()
            : null;
        return buildConfigClass;
    }
}

// FINAL APP WITH IMPLEMENTATION 
// POINTING TO DESIRED CLASS 
public class MyApp extends BaseApp {

    @Override
    protected Class<?> getAppBuildConfigClass() {
        return somefinal.app.package.BuildConfig.class;
    }

}

USAGE IN LIBRARY:

 Class<?> buildConfigClass = BaseApp.getAppBuildConfigClass(Context);
 if(buildConfigClass !- null) {
     // do your job 
 }

*there are couple of things need to be watched out:

  1. getApplicationContext() - could return a context which is not an App ContexWrapper implementation - see what Applicatio class extends & get to know of the possibilities of context wrapping
  2. the class returned by final app could be loaded by different class loaders than those who will use it - depends of loader implementation and some principals typical (chierarchy, visibility) for loaders
  3. everything depends on the implemmentation of as in this case simple DELEGATION!!! - the solution could be more sophisticetaded - i wanted only to show here the usage of DELEGATION pattern :)

** why i downwoted all of reflection based patterns because they all have weak points and they all in some certain conditions will fail:

  1. Class.forName(className); - because of not speciified loader
  2. context.getPackageName() + ".BuildConfig"

a) context.getPackageName() - "by default - else see b)" returns not package defined in manifest but application id (somtimes they both are the same), see how the manifest package property is used and its flow - at the end apt tool will replace it with applicaton id (see ComponentName class for example what the pkg stands for there)

b) context.getPackageName() - will return what the implementaio wants to :P

*** what to change in my solution to make it more flawless

  1. replace class with its name that will drop the problems wchich could appear when many classes loaded with different loaders accessing / or are used to obtain a final result involving class (get to know what describes the equality between two classes (for a compiler at runtime) - in short a class equality defines not a self class but a pair which is constituted by the loader and the class. (some home work - try load a inner class with different loader and access it by outer class loaded with different loader) - it would turns out that we will get illegal access error :) even the inner class is in the same package has all modificators allowing access to it outer class :) compiler/linker "VM" treats them as two not related classes...
Gutter answered 4/6, 2018 at 19:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.