Guide to implement Mobile Services from different Providers in single Codebase | Build Variants & Product Flavors
In this article, I will try to explain how to use different build variants by implementing product flavors to your project. At the end of the article, you can find GitHub link to the demo project.
1 — What is Build Variant
Build variant is a feature of Android Studio to create a project which can be build in different versions (flavors)
2 — What are the Benefits
By using Build Variants, you can create multile different versions of your apk by only using single codebase.
For example you can create;
- Different apks for demo and full version of your project
- Different apks for Huawei and Google implementations
- Different apks for different api levels
3 — How to Implement
Before You Start!
In This Example, I will use different build variants for implementing Huawei & Google maps in demo project. I am assuming you already know the implementation of the HMS and GMS Map Kits so I will not cover how to implement them in detail.
So before you start, you need to know ;
- Kotlin
- How to implement Huawei Map
- How to implement Google Map
Let’s Start
How can I see which variant I am using right now?
Android Studio offers two different build variants by default “debug” and “release”. Current active build variant can be found on the left handside of the Android Studio in Build Variants tab.
Let’s create more variants
To add custom build variants to your project, we need to open your App level “build.gradle” file and add our our productFlavors between the android{} brackets
flavorDimensions "provider"
productFlavors
{
huawei
{
dimension "provider"
versionNameSuffix "HMS"
}
google
{
dimension "provider"
versionNameSuffix "GMS"
}
}
After configuring our gradle file, wait it to sync and we will have our new variants at the end of synchronization.
Note: The same implementation of the variants can also be done through Build > Edit Flavors menu of the Android Studio
If you realized, there are variety of options for costumization such as app id, target sdk version etc. You also can add those customizations to your “build.gradle” file manually
Arranging Variant Specific Project Folders / Source Sets
When you create a new Android Studio Project, it automatically creates the main/ source set and it’s subdirectories.
main/ folder holds everything you want to share between all your build variants. So we will code our project’s base here inside of the main/ folder. Whatever we put inside of the main folder, it will stay same among all the variants.
For other variants, gradle expects you to organize your directory in a similar way to the main/ directory. To see how to organize your files for each of your build variant, gradle provides a useful task that shows you how to organize your files. To find it, click on Gradle button (most of the time it is at the right side of your IDE) then navigate to the
Tasks > android > sourceSets. Double click on it and gradle will execute the task and give you all the folder structue it is expecting from you for different build variants.
Let’s look at our Huawei variant’s expected folder strucrure and start creating it.
So gradle is expecting us to create “huawei” folder and it’s subfolders under the src/ directory to implement our Huawei variant.
You can add new directories manually or, you can right click on the app folder, then navigate to New > Folder > Java Folder (or other folders based on your needs)
And then simply select your Target Source Set and gradle will create the directory for you.
We can create required folders for our other variants by using the same way. After creating all of them, our project structure will look like this;
Note: To see all variant folders at once do not forget to select Project instead of Android from the dropdown menu of the Project tab, otherwise you will only see the folders of the active variant.
Understanding the Project Structure
As you can see from the picture above, we have one MainActivity class and two seperate MapHelper class.
MainActivity Class
class MainActivity : AppCompatActivity() { private val REQUEST_CODE = 376
private lateinit var mapHelper: MapHelper
private val RUNTIME_PERMISSIONS = arrayOf<String>(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.INTERNET
) override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(com.ibrahimrecepserpici.buildvariantmapsdemo.R.layout.activity_main) if (!hasPermissions(this, *RUNTIME_PERMISSIONS ))
{
askForPermissions()
}else
{
mapHelper =
MapHelper(
this
)
}
}
.........
}
This class is the base class for our project which means it will be the same does not matter which variant we are using.
As you can see the basic function of this class is launching our activity, asking the required permissions for map kit and if user gives the required permissions, initalize MapHelper class so it can be seen on the screen.
MapHelper Class
We have 2 implementations of MapHelper class since implementations of Huawei Maps and Google Maps has slight differences. (Actually only the dependencies are different for this case but for different cases, probably there will be much more difference between variant classes )
1-For Huawei Variant
package com.ibrahimrecepserpici.buildvariantmapsdemoimport androidx.appcompat.app.AppCompatActivity
import com.huawei.hms.maps.HuaweiMap
import com.huawei.hms.maps.SupportMapFragment
import com.huawei.hms.maps.model.LatLng
import com.huawei.hms.maps.model.MarkerOptions
import com.ibrahimrecepserpici.buildvariantmapsdemo.Rclass MapHelper { private lateinit var map : HuaweiMap
private var mActivity : AppCompatActivity constructor(activity: AppCompatActivity)
{
mActivity = activity
initMap()
}
fun initMap()
{
val mapFragment = (mActivity.supportFragmentManager.findFragmentById(R.id.mapFragment) as SupportMapFragment)
mapFragment.getMapAsync {
map = it
map.addMarker(MarkerOptions().position(LatLng(6.2186, -75.5742)).title("HUAWEI")).snippet = "Hello From Maps!"
}
}
}
2-For Google Variant
package com.ibrahimrecepserpici.buildvariantmapsdemoimport android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import com.ibrahimrecepserpici.buildvariantmapsdemo.Rclass MapHelper { private lateinit var map : GoogleMap
private var mActivity : AppCompatActivity constructor(activity: AppCompatActivity)
{
mActivity = activity
initMap()
}
fun initMap()
{
val mapFragment = (mActivity.supportFragmentManager.findFragmentById(R.id.mapFragment) as SupportMapFragment)
mapFragment.getMapAsync {
map = it
map.addMarker(MarkerOptions().position(LatLng(6.2186, -75.5742)).title("GOOGLE")).snippet = "Hello From Maps!"
Log.e("TEST","GMAP CREATED")
}
}
}
Depending on which variant is active, Android Studio will pick the relevant MapHelper class and build the project accordingly.
Layout Files
Since Huawei and Google different dependencies, we need to prepare layouts seperately for each variant and put those layouts to the correct directories respectively.
If you take a quick look to both layout files, you can see fragments are coming from different dependencies.
1-Huawei Variant
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
xmlns:map="http://schemas.android.com/apk/res-auto"
android:id="@+id/mapFragment"
class="com.huawei.hms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
map:cameraTargetLat="6.2486"
map:cameraTargetLng="-75.5742"
map:cameraZoom="12" />
</androidx.constraintlayout.widget.ConstraintLayout>
2-Google Variant
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
xmlns:map="http://schemas.android.com/apk/res-auto"
android:id="@+id/mapFragment"
class="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
map:cameraTargetLat="6.2486"
map:cameraTargetLng="-75.5742"
map:cameraZoom="12" />
</androidx.constraintlayout.widget.ConstraintLayout>
Manifest Files
1-Google Variant
For the Google variant, our manifest file needs to include api key as meta data, so it is going to be different than Huawei/Generic variant
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ibrahimrecepserpici.buildvariantmapsdemo"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"> <meta-data android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key"/> <activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application></manifest>
We also have googlestring.xml file at the google/ source set to hold our api key for map.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="google_maps_key">AIzaSyBhVBKFUKdUv1HJ-elXuqm6IaNpxAag08k</string>
</resources>
2- Huawei Variant
We do not need to generate an extra manifest for Huawei variant because Huawei does not require any special implementation. Because of we are not adding any extra manifest for Huawei variant, it will use main/ source set’s manifest upon build.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ibrahimrecepserpici.buildvariantmapsdemo"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application></manifest>
Adding More flavorDimensions
Until now we had only one dimension called “provider” to implement variants based on different providers.
What if we also want to add different variants for different android api levels?
This is where flavorDimensions comes in handy. Adding more dimensions will allow gradle to create all possible combination of variants of the different dimensions.
We simply need to add one more dimension in our “build.gradle” file and configure our project accordingly.
And after that we can add our api variants in our productFlavors
flavorDimensions "provider", "api"
productFlavors
{
huawei
{
dimension "provider"
versionNameSuffix "HMS"
}
{
dimension "provider"
versionNameSuffix "GMS"
} api24
{
dimension "api" }
api27
{
dimension "api"
}
}
After this step you need to repeat the same steps above, create directories for new variants and add new variant specific files respectively.
Configuring Variant Filters
Gradle automatically creates all the possible combination of the variants in different dimensions that you created. However, maybe you do not need some of the variants to exists because of they are either makes no sense or they are no necessary.
For example, in this demo project, lets assume we do not want to have Huawei Api27 variant. Let’s filter it. All you need to do is add variantFilter to your “build.gradle” file between the android{} brackets.
variantFilter
{ variant ->
def names = variant.flavors*.name if (names.contains("api27") && names.contains("huawei"))
{
setIgnore(true)
}
}
After that, hit the sync button and let the magic happen.
As you can see, we do not have variant for Huawei Api27 anymore.
Final Result of the Project
4 — Resources
GitHub Link for Demo Project : https://github.com/Disav0wed/BuildVariantMapsDemo