🔧 Implement Custom Class in Android BuildConfig field

👋 Hi, thank you for visiting my personal blog & hopefully you all in good health. in this article I would like to share about BuildConfig in Android and how do we add custom class in BuildConfig field.

What is Android BuildConfig?

At build time, Gradle generates the BuildConfig class so your app code can inspect information about the current build. Inside the default BuildConfig will be like the following example

1 2 3 4 5 6 7 8 9 package id.yuana.playground; public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "id.yuana.playground"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = 1; public static final String VERSION_NAME = "1.0"; }

Note: since AGP 8.0, you need to set the value of android.buildFeatures.BuildConfig to true inside your build.gradle(.kts) in :app module

1 2 3 4 5 6 7 8 9 10 11 //example build.gradle.kts inside your :app module //... android { //... buildFeatures { buildConfig = true } //... } //...

How do we use it?

Many use cases that we can use this BuildConfig, for example:

  • Force update feature by checking BuildConfig.VERSION_CODE & BuildConfig.VERSIOIN_NAME
  • Implement HttpLoggingInterceptor when we build debug build type
  • And many more
  • In this example I will show you how to use it by implementing HttpLoggingInterceptor

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //let say we have NetworkModule like following //this example using Hilt @Module @InstallIn(SingletonComponent::class) class NetworkModule { @Provides @Singleton fun provideOkHttpClient( prefs: Preferences ): OkHttpClient = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { setLevel(when { BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY else -> HttpLoggingInterceptor.Level.NONE }) }) .addInterceptor { val req = it.request().newBuilder() .addHeader("Accept", "application/json") .addHeader("Authorization", prefs.getToken()) .build() it.proceed(req) } .build() //... }

    As we can see the snippet above, that debug build type, will implement log level HttpLoggingInterceptor.Level.BODY otherwise will be HttpLoggingInterceptor.Level.NONE or we can say it no logs will be printed.

    It’s easy to use BuildConfig, just call it in your code and do what do you want to do.

    How do we add custom BuildConfig field?

    In this section I will show you how to add custom BuildConfig field, so you can add more specific field for your use cases.

    For example, We can add BASE_URL or API_KEY into BuildConfig from gradle.properties file

    gradle.properties

    1 2 3 4 # ... movieBaseUrl="https://api.yourservice.com/v1/" movieApiKey="yourApiKey"

    build.gradle.kts app module

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //... android { //... buildTypes { val movieBaseUrl: String by project val movieApiKey: String by project buildTypes.onEach { it.buildConfigField("String", "MOVIE_BASE_URL", movieBaseUrl) it.buildConfigField("String", "MOVIE_API_KEY", movieApiKey) } } buildFeatures { compose = true aidl = false buildConfig = true renderScript = false shaders = false } //... } //...

    As you can see the above snippet code, we can also add custom fields to the BuildConfig class from your Gradle build configuration file using the buildConfigField() method and access those values in your app's runtime code.

    The BuildConfig class will looks like the following

    1 2 3 4 5 6 7 8 9 10 11 12 13 package id.yuana.playground; public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "id.yuana.playground"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = 1; public static final String VERSION_NAME = "1.0"; // Field from build type: debug public static final String MOVIE_BASE_URL = "https://api.yourservice.com/v1/"; // Field from build type: debug public static final String MOVIE_API_KEY = "yourApiKey"; }

    buildConfigField method

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * Adds a new field to the generated BuildConfig class. * * * The field is generated as: `<type> <name> = <value>;` * * * This means each of these must have valid Java content. If the type is a String, then the * value should include quotes. * * @param type the type of the field * @param name the name of the field * @param value the value of the field */ fun buildConfigField(type: String, name: String, value: String)

    I just copy-paste about the snippet code above from the documentation, it contains explanation that will helps you.

    From the above documentation, please take a look at this

    1 2 3 4 The field is generated as: `<type> <name> = <value>;` This means each of these must have valid Java content. If the type is a String, then the value should include quotes.

    I will show you when we add buildConfigField() method and the field will be generated by using the following examples

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 //1. example using String type buildConfigField("String", "MOVIE_API_KEY", "\"yourApiKey\"") //the above code will generate public static final String MOVIE_API_KEY = "yourApiKey"; //2. example using int type buildConfigField("int", "VERSION_MINOR", "1") //the above code will generate public static final int VERSION_MINOR = 1; //3. example using Float type buildConfigField("Float", "NGANU_FLOAT_VALUE", "10f") //the above code will generate public static final Float NGANU_FLOAT_VALUE = 10f; //4. example using Long type buildConfigField("Long", "NGANU_LONG_VALUE", "80L") //the above code will generate public static final Long NGANU_LONG_VALUE = 80L; //so you should be able call it like the following code BuildConfig.MOVIE_API_KEY BuildConfig.VERSION_MINOR BuildConfig.NGANU_FLOAT_VALUE BuildConfig.NGANU_LONG_VALUE

    another way to add BuildConfig field, you can check it here.

    1 2 3 4 5 6 7 8 9 10 11 12 //we can also add other type, for example List like the following buildConfigField( "java.util.List<String>", "WORK_DAYS", "java.util.Arrays.asList(\"monday\", \"wednesday\", \"friday\")" ) //it will generate field inside BuildConfig like the following public static final java.util.List<String> WORK_DAYS = java.util.Arrays.asList("monday", "wednesday", "friday"); //so you can call it by using BuildConfig.WORK_DAYS

    Back to our case, add MOVIE_BASE_URL and MOVIE_API_KEY. Now you can get the information about baseUrl and apiKey inside your code like the following snippet code

    NetworkModule.kt

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply { setLevel( when { BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY else -> HttpLoggingInterceptor.Level.NONE } ) }).addInterceptor { val request: Request = it.request().newBuilder().url( it.request().url.newBuilder() .addQueryParameter("api_key", BuildConfig.MOVIE_API_KEY).build() ).build() it.proceed(request) }.build() @Provides @Singleton fun provideRetrofit(okhttpClient: OkHttpClient): Retrofit = Retrofit.Builder().baseUrl(BuildConfig.MOVIE_BASE_URL).client(okhttpClient) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(ResultCallAdapterFactory.create()).build() @Provides @Singleton fun provideMovieApi(retrofit: Retrofit): MovieApi = retrofit.create(MovieApi::class.java) }

    As we can see, you can get the value of your custom BuildConfig field by calling BuildConfig.MOVIE_API_KEY and BuildConfig.MOVIE_BASE_URL.

    How do we add custom class as type in BuildConfig field?

    This is the interesting part that we can add custom class for the BuildConfig field 😁

    How to do it?

  • first, you need to create your custom class
  • then use it inside your BuildConfig field inside yor gradle build script file 😄
  • after gradle sync successfully, voila! now you can use it inside your code 🎉
  • Let me show you how to do it with an example.

    Example

    Let’s say we have 4 activity screen:

  • SplashActivity → the screen that will be displayed first time
  • LoginActivity → the screen will be displayed if the users not login yet
  • HomeActivity → the screen will be displayed if the users already login
  • ProfileActivity → the screen will be displayed after we already at HomeActivity
  • Please assume that screen or activity above represents each module and each module have specific team. We need to by pass login process in the LoginActivity to increase feedback loop and more productive in development mode. so the engineer can be focus on the module that they own.

    How to do that? simple, we need a variable that hold activity class and the credentials of the user (e.g. email & password).

    Let’s say we can configure what to display after SplashActivity in development by using some configuration file.

    entrypoint-config.properties

    1 2 3 4 enabled=true entryPoint.activity=id.yuana.buildconfig.demo.presentation.ProfileActivity [email protected] entryPoint.password=12345678

    EntryPoint.kt

    1 2 3 4 5 6 7 8 9 10 package id.yuana.buildconfig.demo.config /** * describe [EntryPoint] will be hold entry point after splash */ class EntryPoint( val activityClass: Class<*>, val email: String, val password: String )

    build.gradle.kts (:app)

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //load entrypoint-config.properties val entryPointProps = loadProperties("${projectDir.parent}/entrypoint-config.properties") //inside buildTypes DSL like below buildTypes { release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } buildTypes.onEach { it.buildConfigField("Boolean", "ENTRY_POINT_ENABLED", entryPointProps["enabled"] as String) //here we go, the custom class for BuildConfig field here it.buildConfigField("id.yuana.buildconfig.demo.config.EntryPoint", "ENTRY_POINT_CONFIG", "new id.yuana.buildconfig.demo.config.EntryPoint(${entryPointProps["entryPoint.activity"] as String}.class, \"${entryPointProps["entryPoint.email"] as String}\", \"${entryPointProps["entryPoint.password"] as String}\")") } }

    SplashActivity.kt

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class SplashActivity : AppCompatActivity() { private val repository by lazy { (application as App).repository } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_splash) lifecycleScope.launch { delay(500) if (BuildConfig.ENTRY_POINT_ENABLED) { needByPassAndDetermineNextScreen() } else { determineNextScreen() } } } private fun needByPassAndDetermineNextScreen() { //this is from our BuildConfig field with custom class val entryPointConfig = BuildConfig.ENTRY_POINT_CONFIG if (repository.alreadyLogin()) { startActivity(Intent(this, entryPointConfig.activityClass)) finish() } else { val success = repository.login( email = entryPointConfig.email, password = entryPointConfig.password ) if (success) { startActivity(Intent(this, entryPointConfig.activityClass)) finish() } else { Toast.makeText(this, getString(R.string.msg_login_failed), Toast.LENGTH_LONG).show() determineNextScreen() } } } private fun determineNextScreen() { val nextIntent = if (repository.alreadyLogin()) { HomeActivity.createIntent(this) } else { LoginActivity.createIntent(this) } startActivity(nextIntent) finish() } }

    More detail you can clone this project here andhikayuana/android-buildconfig-demo (github.com)

    Demo Video available at https://youtu.be/JsUi2ymWtfc

    Conclusion

    In my view, this is powerful and I will share with case in the next article, see you!

    Feedback

    You can give feedback on this article by contacting me, Thank you!

    References

  • AGP 8.0 - buildFeatures.buildConfig
  • AGP 7.4 - buildFeatures.buildConfig
  • HttpLoggingInterceptor.Level.NONE
  • HttpLoggingInterceptor.Level.BODY
  • Example: andhikayuana/movieapp-compose
  • Docs: VariantDimension#buildConfigField
  • android/gradle-recipes · GitHub
  • ← Go home