Manifesting appName the right way
Recently, I was tasked to integrate an open source library into our project, and we did successfully so. Well, we thought we did until we saw that our app name was shown as empty strings in the logs. We know for sure we have an app name. If we didn’t have one, our app name wouldn’t show up in Android Launchers. So naturally, our first place we look was the AndroidManifest
. To double confirm it, we also checked the merged manifest which is basically the final manifest file after compiling all the libraries. These check proves that we are setting the app name correctly. If the fault is not on our side, then it could be from the library itself. Hence, we started looking into the source of the tool,and we saw this line of code for accessing app name.
val appName: String = if (context.applicationInfo.labelRes != 0) context.getString(context.applicationInfo.labelRes) else ""
The documentation said this returns a resource id of the app name, and if it doesn’t exist, it returns 0.
A string resource identifier (in the package’s resources) of this component’s label. From the “label” attribute or, if not set, 0.
We replicated exactly the way we setup our app name in a separate dummy project, copy-pasted that line of code to see if it was the culprit. Turns out it’s returning 0
for our string resource, which in turns, returns empty string! We started googling and looking through Android developer docs, and we found out there's another variable for app name which is nonLocalizedLabel
.We wanted to see what was the difference with the labelRes
. Thus, we tested nonLocalizedLabel
out and the function no longer returns an empty string. So, what exactly was the difference?
Let’s paddle back a little. Normally we would have our app name in AndroidManifest
through app_name
string resource. In this scenario, labelRes
would returns a valid string resource and everything will be rainbow and sunshine.
<manifest><application
<!--Other props here-->
android:label="@string/app_name" >
</application></manifest>
But that isn’t always the case. Sometimes we want our app to have different name based on different flavors, build types, regions etc. For example, our app can have different app name based on build types and build flavor. The easiest way to have this is through Manifest Placeholders.
<application
android:label="${flavorName}-${buildTypeName}">
</application>//build.gradle
productFlavors {
apple {
manifestPlaceholders = [flavorName: "Apple"]
}
banana {
manifestPlaceholders = [flavorName: "Banana"]
}
}buildTypes {
staging {
manifestPlaceholders = [buildTypeName: "Staging"]
}
release {
manifestPlaceholders = [buildTypeName: ""]
}
}
You can start to see a hint of where I’m going. Since we are overriding the manifest on build directly through placeholders, we are no longer using any string resources. It does now make sense that we are getting an empty string previously. We are not using any string resources, so labelRes
returns 0
. Any app label that doesn’t use string resource will be considered as non-localized string since they don’t change dynamically based on locale of the device. In this case, nonLocalizedString
will have a non-empty string.
Working around the issue
This is easy to patch, instead of returning empty string as fallback, we just return nonLocalizedString
. That way, we make sure the app name can never be empty or null. But however, since we are depending on a third party library, it would take some time for our PR to be merged and for new version to be released.
While we wait for that, we also came up with a workaround to mediate this problem. Instead of using manifestPlaceholder
, we could generate a resource field for app name through Gradle. To refresh, what we wanted is to combine a string from two different sources; product flavor and build type. First, we need to declare the variables for each sources.
productFlavors.all {
ext.flavorName = null
}
buildTypes.all {
ext.buildTypeName = null
}
After this, we can set the value to each variable.
productFlavors {
apple {
flavorName = "Apple"
}
banana {
flavorName = "Banana"
}
}buildTypes {
staging {
buildTypeName = "Staging"
}
release {
buildTypeName = ""
}
}
And then we can append these twos with the following code:
applicationVariants.all { variant ->
variant.resValue "string", "app_name", '"' + variant.productFlavors.get(0).flavorName + '-' + variant.buildType.buildTypeName + '"'
}
Combining all of these, we get the following:
This allows us to generate a resource value that combines the string from two sources and “fixes” the issue for us, now that it has a valid string resource.
To sum it up, if you’re a library developer and need to access app name, be sure to consider the case for both resources id and a non-localized string. We should prioritize the former case, since the app name can be localized and if it does, it would have a resource id associated with it. Use nonLocalizedString
as a fallback if and only if the resource id does not exist, i.e, labelRes
returns 0
. And finally, if you’re like me facing this issue with an integration not in your control, hope this workaround helps.