web-dev-qa-db-fra.com

Gradle: comment utiliser BuildConfig dans une bibliothèque Android avec un indicateur qui est défini dans une application

Mon (gradle 1.10 et gradle plugin 0.8) -based Android project se compose d'une grande bibliothèque Android qui est une dépendance pour 3 applications Android différentes

Dans ma bibliothèque, j'aimerais pouvoir utiliser une structure comme celle-ci

if (BuildConfig.SOME_FLAG) {
    callToBigLibraries()
}

comme proguard pourrait réduire la taille de l'apk produit, basé sur la valeur finale de SOME_FLAG

Mais je ne peux pas comprendre comment le faire avec gradle comme:

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

J'ai essayé sans succès de jouer avec des BuildTypes et des trucs comme

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

Quelle est la bonne façon de créer un BuildConfig partagé pour ma bibliothèque et mes applications dont les indicateurs seront remplacés lors de la création dans les applications?

53
Lakedaemon

Vous ne pouvez pas faire ce que vous voulez, car BuildConfig.SOME_FLAG Ne sera pas propagé correctement dans votre bibliothèque; les types de build eux-mêmes ne sont pas propagés aux bibliothèques - ils sont toujours construits en tant que RELEASE. C'est un bug https://code.google.com/p/Android/issues/detail?id=52962

Pour contourner ce problème: si vous avez le contrôle sur tous les modules de la bibliothèque, vous pouvez vous assurer que tout le code touché par callToBigLibraries() est dans des classes et des packages que vous pouvez nettoyer proprement avec ProGuard, puis utilisez la réflexion afin que vous puissiez y accéder s'ils existent et se dégrader gracieusement s'ils ne le font pas. Vous faites essentiellement la même chose, mais vous effectuez la vérification au moment de l'exécution au lieu de la compilation, et c'est un peu plus difficile.

Faites-moi savoir si vous avez du mal à trouver comment procéder; Je pourrais vous fournir un échantillon si vous en avez besoin.

19
Scott Barta

Pour contourner ce problème, vous pouvez utiliser cette méthode, qui utilise la réflexion pour obtenir la valeur du champ depuis l'application (et non la bibliothèque):

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

Pour obtenir le champ DEBUG, par exemple, il suffit d'appeler cela depuis votre Activity:

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

J'ai également partagé cette solution sur le AOSP Issue Tracker .

42
Phil

La solution/solution de contournement suivante fonctionne pour moi. Il a été publié par un gars dans le suivi des problèmes de Google:

Essayez de définir publishNonDefault sur true dans le projet bibliothèque:

Android {
    ...
    publishNonDefault true
    ...
}

Et ajoutez les dépendances suivantes au projet app qui utilise la bibliothèque:

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

De cette façon, le projet qui utilise la bibliothèque inclut le type de génération correct de la bibliothèque.

32
AllThatICode

Dans le cas où l'ID d'application n'est pas le même que le package (c'est-à-dire plusieurs ID d'application par projet) ET que vous souhaitez accéder à partir d'un projet de bibliothèque:

Utilisez Gradle pour stocker le package de base dans les ressources.

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

En 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);
}
4
Mark

J'utilise une classe BuildConfigHelper statique dans l'application et la bibliothèque, afin de pouvoir définir les packages BuildConfig comme variables statiques finales dans ma bibliothèque.

Dans l'application, placez une classe comme celle-ci:

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;

}

Et dans la bibliothèque:

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

}

Ensuite, n'importe où dans votre bibliothèque où vous souhaitez vérifier BuildConfig.DEBUG, vous pouvez vérifier BuildConfigHelper.DEBUG et y accéder de n'importe où sans contexte, et de même pour les autres propriétés. Je l'ai fait de cette façon pour que la bibliothèque fonctionne avec toutes mes applications, sans avoir besoin de passer un contexte ou de définir le nom du package d'une autre manière, et la classe d'application n'a besoin que de modifier la ligne d'importation pour l'adapter à l'ajout dans un nouveau application

Edit: je voudrais simplement réitérer que c'est la façon la plus simple (et la seule répertoriée ici) d'obtenir les valeurs à affecter aux variables statiques finales de la bibliothèque à partir de toutes vos applications sans avoir besoin d'un contexte ou d'un codage en dur de la le nom du package quelque part, qui est presque aussi bon que d'avoir les valeurs dans la bibliothèque par défaut BuildConfig de toute façon, pour l'entretien minimal de changer cette ligne d'importation dans chaque application.

3
Kane O'Riley

utilise les deux

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

Je veux appeler comme ci-dessous

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

pour moi, c'est la SEULE SOLUTION UNIQUE ET ACCEPTABLE * POUR déterminer le 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;
    }

}

TILISATION DANS LA BIBLIOTHÈQUE:

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

* il y a quelques choses à surveiller:

  1. getApplicationContext () - pourrait renvoyer un contexte qui n'est pas une implémentation App ContexWrapper - voir ce que la classe Applicatio étend et connaître les possibilités de l'habillage du contexte
  2. la classe retournée par l'application finale pourrait être chargée par des chargeurs de classe différents de ceux qui l'utiliseront - dépend de l'implémentation du chargeur et de certains principes typiques (hiérarchie, visibilité) pour les chargeurs
    1. tout dépend de l'implémentation de comme dans ce cas simple DELEGATION !!! - la solution pourrait être plus sophistiquée - je voulais seulement montrer ici l'utilisation du modèle DELEGATION :)

** pourquoi j'ai rétrogradé tous les modèles basés sur la réflexion car ils ont tous des points faibles et ils échouent tous dans certaines conditions:

  1. Class.forName (className); - en raison du chargeur non spécifié
  2. context.getPackageName () + ".BuildConfig"

    a) context.getPackageName () - "par défaut - sinon voir b)" renvoie non le package défini dans le manifeste mais l'identifiant de l'application (parfois ils sont tous les deux les mêmes), voyez comment la propriété du package manifeste est utilisée et son flux - à la fin L'outil apt le remplacera par l'ID applicaton (voir la classe ComponentName par exemple ce que le paquet représente là)

    b) context.getPackageName () - retournera ce que l'implémentaio veut: P

*** quoi changer dans ma solution pour la rendre plus parfaite

  1. remplacer la classe par son nom qui supprimera les problèmes qui pourraient apparaître lorsque de nombreuses classes chargées avec différents chargeurs accédant/ou sont utilisées pour obtenir un résultat final impliquant la classe (savoir ce qui décrit l'égalité entre deux classes (pour un compilateur à l'exécution) - en bref a l'égalité de classe définit non pas une auto-classe mais une paire qui est constituée par le chargeur et la classe (certains travaux à domicile - essayez de charger une classe interne avec un chargeur différent et d'y accéder par la classe externe chargé avec un chargeur différent) - il se trouve que nous obtiendrons une erreur d'accès illégal :) même la classe interne est dans le même paquet a tous les modificateurs permettant d'accéder à sa classe externe :) compilateur/éditeur de liens "VM" les traite comme deux non classes connexes ...

j'écrirai plus sur les problèmes que nous pourrions rencontrer ici sous peu quand j'aurai du temps libre ...

0
ceph3us