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
}
*есть несколько вещей, которые необходимо следить:
- getApplicationContext () - может вернуть контекст, который не является реализацией ContexWrapper приложения-посмотрите, что класс Applicatio расширяет и узнает о возможностях контекстной упаковки
- в класс, возвращаемый final app, может быть загружен разными загрузчиками классов, чем те, кто будет его использовать - зависит от реализации загрузчика и некоторых принципов, типичных (chierarchy, visibility) для загрузчиков
- все зависит от имплемментации как в данном случае простого делегирования!!! - решение может быть более изощренным - я хотел только показать здесь Использование шаблона делегирования:)
** Почему я downwoted все на основе отражения шаблонов, потому что все они имеют слабые места, и все они в некоторых определенных условиях потерпят неудачу:
- класса.forName(имякласса); - из-за не speciified погрузчик
-
контексте.getPackageName() + ".BuildConfig"
a) контекст.getPackageName () - "по умолчанию-else see b)" возвращает не пакет, определенный в манифесте, а идентификатор приложения (somtimes они оба одинаковы), см. Как используется свойство manifest package и его flow-в конце инструмент apt заменит его идентификатором applicaton (см., например, класс ComponentName, что означает pkg)
б) контекст.getPackageName () - вернет то, что implementaio хочет :P
*** что изменить в моем решении, чтобы сделать его более безупречной
- replace class с его именем, которое отбросит проблемы, которые могут появиться, когда многие классы, загруженные различными загрузчиками, обращаются / или используются чтобы получить конечный результат с участием класса (узнать, что описывает равенство между двумя классами (для компилятора во время выполнения) - короче равенство классов определяет не самостоятельный класс, а пару, которая состоит из загрузчика и класса. (некоторая домашняя работа - попробуйте загрузить внутренний класс с другим загрузчиком и получить доступ к нему внешним классом, загруженным с другим загрузчиком) - окажется, что мы получим ошибку незаконного доступа:) даже внутренний класс в одном пакете имеет все модификаторы, разрешающие доступ к нему внешнему классу:) компилятор / компоновщик " VM " рассматривает их как два не связанных класса...
Я напишу больше о проблемах, с которыми мы могли бы столкнуться здесь в ближайшее время, когда я получу свободное время ...