š§Ā 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:
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?
Let me show you how to do it with an example.
Example
Letās say we have 4 activity screen:
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!