Gradle: как использовать BuildConfig в библиотеке Android с флагом, который устанавливается в приложении

Мой (gradle 1.10 и gradle plugin 0.8) проект Android на основе Android состоит из большой библиотеки Android, которая является зависимостью для 3 различных приложений Android.

В моей библиотеке я хотел бы иметь возможность использовать такую ​​​​структуру

if (BuildConfig.SOME_FLAG) {
    callToBigLibraries()
}

поскольку proguard сможет уменьшить размер создаваемого apk на основе конечного значения SOME_FLAG.

Но я не могу понять, как это сделать с помощью gradle:

* 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.

Я безуспешно пытался играть с BuildTypes и тому подобное

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

Как правильно создать общий BuildConfig для моей библиотеки и моих приложений, чьи флаги будут переопределены при сборке в приложениях?


person Lakedaemon    schedule 26.01.2014    source источник


Ответы (7)


Вы не можете делать то, что хотите, потому что BuildConfig.SOME_FLAG не будет должным образом распространяться в вашей библиотеке; Сами типы сборки не распространяются на библиотеки — они всегда собираются как RELEASE. Это ошибка https://code.google.com/p/android/issues/detail?id=52962

Чтобы обойти это: если у вас есть контроль над всеми библиотечными модулями, вы можете убедиться, что весь код, затронутый callToBigLibraries(), находится в классах и пакетах, которые вы можете аккуратно отделить с помощью ProGuard, а затем использовать отражение, чтобы вы могли получить к ним доступ если они существуют и изящно деградируют, если они не существуют. По сути, вы делаете то же самое, но вы выполняете проверку во время выполнения, а не во время компиляции, и это немного сложнее.

Дайте мне знать, если у вас возникнут проблемы с выяснением того, как это сделать; Я мог бы предоставить образец, если вам это нужно.

person Scott Barta    schedule 29.01.2014
comment
Я понимаю. Спасибо за ответ: я буду следить за связанной проблемой и вернусь к вопросу, как только что-то с этим будет сделано. На данный момент, чтобы обойти эту проблему, я сделал что-то вроде вашего предложения, и код, который я не хотел, действительно был удален. - person Lakedaemon; 31.01.2014
comment
Вы можете просто переопределить вариант сборки LIbrary с помощью `выпуска defaultPublishConfig` или defaultPublishConfig "debug" - person IgorGanapolsky; 23.09.2016

В качестве обходного пути вы можете использовать этот метод, который использует отражение для получения значения поля из приложения (а не из библиотеки):

/**
 * 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;
}

Например, чтобы получить поле DEBUG, просто вызовите это из своего Activity:

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

Я также поделился этим решением в отслеживании проблем AOSP.

person Phil    schedule 12.08.2014
comment
BuildConfig гарантированно будет в этом пакете? - person Vlad; 15.01.2015
comment
@ Влад Я полагаю, что есть способ изменить вывод BuildConfig, однако, исходя из моего опыта, это гарантировано для настройки по умолчанию. - person Phil; 15.01.2015
comment
Это должен быть принятый ответ, потому что он работает! - person IgorGanapolsky; 03.04.2015
comment
Кажется, это работает нормально, пока ваш applicationId не отличается от имени пакета ваших классов Java. (Например, если вы создаете варианты, как указано здесь: tools.android.com/tech-docs/new-build-system/). Кажется, что context.getPackageName() вернет applicationId, который теперь может не быть пакетом ваших классов Java, в котором находится BuildConfig.class. Есть ли обходной путь для этого? - person NPike; 26.06.2015
comment
@NPike, вы можете указать класс в своем основном варианте, который имеет метод по умолчанию, а затем в других каталогах вариантов переопределить этот класс, и вместо использования context.getPackageName() вы можете указать точную строку. - person Phil; 26.06.2015
comment
@NPike, вы можете найти мой ответ полезным stackoverflow.com/a/31587838/444761 - person Mark; 23.07.2015
comment
Недавно я обнаружил ClassNotFoundException с помощью этого метода: если вы объявите суффикс a в buildConfig вашего градиента (например, если вам нужно то же приложение, но с другим именем пакета, в зависимости от сборки отладки или выпуска для теста), context.getPackageName() вернет имя пакета с суффиксом, но BuildConfig по-прежнему будет развернут на имени пакета без суффикса. Итак, метод будет искать com.example.app.suffix.BuildConfigкласс, когда класс com.example.app.BuildConfig. Любые идеи для этого? - person Tito Leiva; 29.04.2016
comment
Этот метод был многообещающим, но как разработчик библиотеки он ломается, как только разработчик приложения меняет applicationId разновидности. Решения этой проблемы до сих пор нет. - person Sky Kelsey; 25.08.2016
comment
понизить голосование, поскольку Class.getPackageName() - это имя, зависящее от загрузчика класса, - может вернуть вам не совсем то, что вы ожидаете :) назначить доступ для поля - person ceph3us; 24.05.2018
comment
Ломается, когда срабатывает ProGuard! - person ofavre; 08.02.2019
comment
@ofavre вам нужно убедиться, что BuildConfig не запутан - person Phil; 26.02.2020

Обновление: в новых версиях плагина Android Gradle publishNonDefault устарел и больше не действует. Все варианты уже опубликованы.

Следующее решение/обходной путь работает для меня. Это было опубликовано каким-то парнем в системе отслеживания проблем Google:

Попробуйте установить publishNonDefault в true в проекте library:

android {
    ...
    publishNonDefault true
    ...
}

И добавьте следующие зависимости в проект app, использующий библиотеку:

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

Таким образом, проект, использующий библиотеку, включает правильный тип сборки библиотеки.

person AllThatICode    schedule 16.07.2015

Я использую статический класс BuildConfigHelper как в приложении, так и в библиотеке, чтобы пакеты BuildConfig устанавливались как окончательные статические переменные в моей библиотеке.

В приложении разместите такой класс:

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;

}

И в библиотеке:

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;
        }
    }

}

Затем в любом месте вашей библиотеки, где вы хотите проверить BuildConfig.DEBUG, вы можете проверить BuildConfigHelper.DEBUG и получить к нему доступ из любого места без контекста, и то же самое для других свойств. Я сделал это таким образом, чтобы библиотека работала со всеми моими приложениями, без необходимости передавать контекст или задавать имя пакета каким-либо другим способом, а классу приложения нужно только изменить строку импорта, чтобы она подходила при добавлении его в новый заявление

Изменить: я просто хотел бы повторить, что это самый простой (и только один из перечисленных здесь) способ получить значения, которые будут присвоены конечным статическим переменным в библиотеке из всех ваших приложений без необходимости контекста или жесткого кодирования имя пакета где-нибудь, что почти в любом случае хорошо, если иметь значения в библиотеке BuildConfig по умолчанию, для минимального обслуживания изменения этой строки импорта в каждом приложении.

person Kane O'Riley    schedule 02.05.2015
comment
Ссылка на имя класса исправления ( константа BUILD_CONFIG ) в библиотеке не кажется надежным решением этой проблемы. - person StaticBR; 16.12.2015
comment
Имя класса помощника никогда не меняется, поэтому ссылка на фиксированное имя класса — это хорошо, и в этом вся суть помощника. Его задача — получить значения из приложения BuildConfig и сохранить их в классе, имя которого известно библиотеке. - person Kane O'Riley; 17.12.2015

В случае, когда идентификатор приложения не совпадает с пакетом (т. е. несколько идентификаторов приложений для каждого проекта), И вы хотите получить доступ из проекта библиотеки:

Используйте Gradle для хранения базового пакета в ресурсах.

В 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}"
}

В 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);
}
person Mark    schedule 23.07.2015

использовать оба

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"
    }
}

Я хочу позвонить, как показано ниже

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;
    }
}
person ohlab    schedule 03.02.2016

Для меня это ЕДИНСТВЕННОЕ И ПРИЕМЛЕМОЕ* РЕШЕНИЕ ДЛЯ ОПРЕДЕЛЕНИЯ ПРИЛОЖЕНИЯ ANDROID 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;
    }

}

ИСПОЛЬЗОВАНИЕ В БИБЛИОТЕКЕ:

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

* есть несколько вещей, на которые нужно обратить внимание:

  1. getApplicationContext () — может возвращать контекст, который не является реализацией App ContexWrapper — посмотрите, что расширяет класс Applicatio, и узнайте о возможностях переноса контекста
  2. класс, возвращаемый окончательным приложением, может быть загружен другими загрузчиками классов, чем те, кто будет его использовать - зависит от реализации загрузчика и некоторых принципов, типичных для загрузчиков (иерархия, видимость).
  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 :)

** почему я отказался от всех паттернов, основанных на отражении, потому что у всех у них есть слабые места, и все они в некоторых определенных условиях потерпят неудачу:

  1. Class.forName (имя класса); - из-за не указанного загрузчика
  2. context.getPackageName() + .BuildConfig

а) context.getPackageName() - по умолчанию - иначе см. б) возвращает не пакет, определенный в манифесте, а идентификатор приложения (иногда они оба одинаковы), см., как используется свойство пакета манифеста и его поток - в конце инструмент apt заменит его идентификатором приложения (см. класс ComponentName, например, что там означает pkg)

б) context.getPackageName() - вернет то, что хочет реализация: P

*** что изменить в моем решении, чтобы сделать его более безупречным

  1. замените класс на его имя, которое устранит проблемы, которые могут возникнуть, когда многие классы загружаются с помощью разных загрузчиков, обращающихся к/или используются для получения окончательного результата с участием класса (узнайте, что описывает равенство между двумя классами (для компилятора во время выполнения) - вкратце, равенство классов определяет не собственный класс, а пару, состоящую из загрузчика и класса. (небольшая домашняя работа - попробуйте загрузить внутренний класс с другим загрузчиком и получить доступ к нему через внешний класс загружается другим загрузчиком) - получится, что мы получим ошибку недопустимого доступа :) даже внутренний класс находится в одном пакете, имеет все модификаторы, разрешающие доступ к нему внешний класс :) компилятор/компоновщик VM рассматривает их как два не связанных класса ...
person ceph3us    schedule 04.06.2018