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

My (gradle 1.10 и Gradle plugin 0.8)-основанный на 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 для моей библиотеки и моих приложений, флаги которых будут переопределены при сборке в приложениях?

7 ответов


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

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

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


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

/**
 * 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 проблема Tracker.


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

попробуйте publishNonDefault to true на библиотека:

android {
    ...
    publishNonDefault true
    ...
}

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

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

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


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

используйте 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);
}

Я использую статический класс 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.Отладка, вы можете проверить BuildConfigHelper.Отладка и доступ к нему из любого места без контекста, и то же самое для другого свойства. Я сделал это таким образом, чтобы библиотека работала со всеми моими приложениями, без необходимости передавать контекст или устанавливать имя пакета каким-либо другим способом, а классу приложений нужна только строка импорта, измененная в соответствии с ней при добавлении ее в новое приложение

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


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

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

для меня это единственное и приемлемое * решение для определения приложения Android BuildConfig.класс:

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

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

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

    a) контекст.getPackageName () - "по умолчанию-else see b)" возвращает не пакет, определенный в манифесте, а идентификатор приложения (somtimes они оба одинаковы), см. Как используется свойство manifest package и его flow-в конце инструмент apt заменит его идентификатором applicaton (см., например, класс ComponentName, что означает pkg)

    б) контекст.getPackageName () - вернет то, что implementaio хочет :P

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

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

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