Best Practices for Working with Different Build Types
A common scenario in most of our apps is to request REST calls to some back-end endpoints that can differ according to development environment, like dev, staging or production. This post will show some ways to setup your Android project for different build types and how to benefits from Gradle build types mechanism.
#1 Way: The hardcoded (and ugly) way
Suppose that you want to load a feed from http://mybackend.com/feed endpoint. In a multi-environment development flow, the endpoint flavors could be:
- dev: https://dev-api.yourbackend.com/feed
- staging: https://stg-api.yourbackend.com/feed
- production: https://api.yourbackend.com/feed
Considering that you are using Retrofit to make REST calls, then the endpoint interface (in Kotlin ❤) may be like:
interface FeedEndpoint {
@GET("/feed")
fun loadFeed():Call<Feed>
}
And setting your Retrofit object like:
val BASE_URL = "https://dev-api.yourbackend.com"
val retrofit = Retrofit.Builder(context)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder()
.create())).build()val feedEndpoint = retrofit.create(FeedEndpoint::class.java)
feedEndpoint.loadFeed()
You run your app in dev environment and it works like magic! But, if you have to build an APK in staging environment for testing purposes? Then you will need to change the BASE_URL to "http://stg-api.mybackend.com". And if you want to run in production? Guess what?! Then you will need to change the BASE_URL variable again to “https://api.mybackend.com". The main drawback of this approach is that you will have to change the BASE_URL each time you run your app in different environment.
#2 Way: Using string resources for each build type
Instead of hardcoding strings, you have a better solution using string resources. Now you have to create a strings.xml (or whatever name that you choose) for each build type and override the base_url string value, like:
debug/res/values/strings.xml
<resources>
<item name=”base_url”>https://dev-api.yourbackend.com</item></resources>
staging/res/values/strings.xml
<resources>
<item name=”base_url”>https://stg-api.yourbackend.com</item></resources>
main/res/values/strings.xml
<resources>
<item name=”base_url”>https://api.yourbackend.com</item></resources>
Now the retrofit setup code will be like that:
val retrofit = Retrofit.Builder(context)
.baseUrl(context.getString(R.string.base_url))
.addConverterFactory(GsonConverterFactory.create(GsonBuilder()
.create())).build()val feedEndpoint = retrofit.create(FeedEndpoint::class.java)
feedEndpoint.loadFeed()
Now your app can run in any build type without changing the base url. Although this can be a better approach, it has some drawbacks:
- Your endpoint code now has a dependency to a Context in order to access getString() method. If you are using some of architecture patterns like MVP, MVVM, Architecture Components or Clean Architecture, this is a bad practice because it mixes both View and Model and also break the Single Responsibility of SOLID principles.
- You will have to create strings.xml for each build type folder like debug/res/values, staging/res/values and main/res/values. Although it works perfectly is valid to point that alternative string resources must be used for internationalization.
#3 Way: Using Gradle Build Config Fields
The most and recommended way to work with different build types is to use Build Config Fields. The code below shows how to setup the build.gradle (for app module) with those different build types:
The buildConfigField clause receives three parameters: the type, the name of and the value of the field. After building the project, Android Studio will generate BuildConfig.java class with a BASE_URL variable and the value for the underlying build type that the app was built. Now, the Retrofit code will become simply like that:
val retrofit = Retrofit.Builder(context)
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder()
.create())).build()val feedEndpoint = retrofit.create(FeedEndpoint::class.java)
feedEndpoint.loadFeed()
And voila! Now you nevermore will need to change endpoint's related code when you run your app upon different build types!