Managing Google and Huawei differences with flavours
Using Android flavours to manage Google Play Services and Huawei Mobile Services
In this article I will try to explain how Android flavours work and how we can use them to manage Google and Huawei differences and keep a single codebase.
Before we dive in, it is worth noticing that this is something Notificare already does for you. Please consider this post only as an example on how you would approach API differences between these two services with your own code.
1. Build variants
Android Studio generates different build variants based on certain factors, like flavours and build types.
In Google's documentation, the presented example is about an app with two flavours: paid
and free
.
One can assume that certain features are not available in the free version.
In this example, I'd like to the consider the challenge of deploying to both Android stores out there — Google and Huawei.
With the lack of Google Play Services in nowadays Huawei devices, developers need to implement Huawei Mobile Services if they want to support all the devices out there. Consider, we're implementing Push. For the typical Android device, the way forward is to include Firebase Messaging which will not work in the latest Huawei devices. For those devices, we need to implement the Huawei Push Services.
For this scenario, it makes sense to have two different flavours: google
and huawei
.
2. Why take this approach
The key principle behind this approach is that we're able to build a single app without having to replicate business logic for each variant and distribute the app to each store by simply building the appropriate flavour.
In short:
- Single codebase
- Unpolluted APKs for each store
3. How to implement it
First of all, we need to define the available flavours, otherwise we'll get just the default one:
android {
flavorDimensions "provider"
productFlavors {
google {
dimension "provider"
}
huawei {
dimension "provider"
}
}
}
Succinctly, to implement push regardless of the variant, we need to:
- Include some permissions in the manifest
- Implement a service and include it in the manifest
To keep things separate, I'd suggest to build two different library modules. One for the Google-specific components and another for Huawei's. Since these have different types, we'll eventually need some glue code for our core application.
Glue code
This part lives in our core application. The purpose is to have a unified notification object and its handler:
data class MyNotification(
val title: String,
val message: String,
)
object MyPushHandler {
fun onNewToken(token: String) {
// Send the updated FCM / HMS token to your backend.
}
fun onNotificationReceived(notification: MyNotification) {
// Handle the notification. Place it in the drawer if appropriate.
}
}
With this, you're able to define the structure of your notifications regardless of their delivery service.
Additionally, this would be the ideal place to handle the notification. Whether we want to display it in the drawer or to trigger some silent update, we can do so without having to worry how the notification was actually delivered.
Google library module
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
// your application core imports
class GooglePushService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
MyPushHandler.onNewToken(token)
}
override fun onMessageReceived(message: RemoteMessage) {
MyPushHandler.onNotificationReceived(
MyNotification(
title = message.data["title"] ?: "",
message = message.data["message"] ?: "",
)
)
}
}
We can leverage Android Studio's manifest merging features to automatically include this specific service and the required permissions without having to worry about it in the application's manifest. Simply update the manifest of the library module with the following:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="...">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<application>
<service
android:name=".GooglePushService"
android:exported="false"
android:label="Google Push Service">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
Huawei library module
Similarly, for Huawei, we create the service to receive the remote messages and convert them to our unified MyNotification
object:
import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage
// your application core imports
class HuaweiPushService : HmsMessageService() {
override fun onNewToken(token: String) {
MyPushHandler.onNewToken(token)
}
override fun onMessageReceived(message: RemoteMessage) {
MyPushHandler.onNotificationReceived(
MyNotification(
title = message.dataOfMap["title"] ?: "",
message = message.dataOfMap["message"] ?: "",
)
)
}
}
Again, we do the same for the Huawei part of the manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="...">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application>
<service
android:name=".HuaweiPushService"
android:exported="false"
android:label="Huawei Push Service">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT"/>
</intent-filter>
</service>
</application>
</manifest>
Linking everything together
One final step is to include the appropriate plugin and dependency in our application's build.gradle
.
Let's start with the easier part - the library dependency. Gradle already gives us an easy way to conditionally include dependencies based on flavours.
Typically, we'd do something like implementation project(':my-fcm-lib')
but in this case we should do the following instead:
googleImplementation project(':my-fcm-lib')
huaweiImplementation project(':my-hms-lib')
This will make sure we only include the appropriate dependency for the flavour that's being built.
As for including the plugin, sadly Gradle doesn't provide anything out of the box. However, we can check the start parameter of the current Gradle task, which should include the flavour's name and based on that we can apply the correct plugin:
def startParameter = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
if (startParameter.contains("google")) {
println("Applying Google services plugin.")
apply plugin: 'com.google.gms.google-services'
} else if (startParameter.contains("huawei")) {
println("Applying Huawei services plugin.")
apply plugin: 'com.huawei.agconnect'
}
Final thoughts
That's it folks! At this point we should have a working solution capable of receiving push notifications from both platforms without the need to include all the services/dependencies nor a different application for each platform.
The same principle can be applied to other things like Maps, Camera or NFC APIs where these two services differ. We'll cover those in future articles.
As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.