Streamline Builds with Android Manifest Placeholders
Android Manifest file is essential for any Android app, which contains specific information about your app, Android build tools, Google Play, device permissions, app launch information, operating system config, and more. Every Android app must have an AndroidManifest.xml
file in the directory structure.
Android Manifest usually contains pre-defined or static information, which is then used to run the app. However, the Android toolchain provides customization by allowing you to specify dynamic information through variable declaration, generally referred to as Android Manifest placeholders. These variables are specified under the manifestPlaceholders
block in your build.gradle
file.
The variables are defined in key-value pairs and can be used for various purposes, such as specifying custom configurations for your Manifest, managing configurations specific to build variants, and more.
In this article, you will learn how to make use of Android Manifest placeholders to supercharge your Android app development.
Sentry
Android Crash Reporting and Performance Monitoring
Resolve Android crashes faster with deep insights and automated workflows.
Injecting Variables into AndroidManifest.xml
Let's begin with the simplest case. When you need to specify a property, such as a URL for your app's network communication in your Manifest, you need to specify it in your build.gradle
as below:
// build.gradle
android {
defaultConfig {
manifestPlaceholders.baseUrl = "api.demoserver.com"
}
}
Here, the variable name is baseUrl
, whereas the assigned string is the value that will be received by AndroidManifest.xml
once declared as below:
// AndroidManifest.xml
<intent-filter ...>
<data android:scheme="https" android:host="${baseUrl}" ... />
</intent-filter>
Similarly, other examples can include properties such as applicationId
, screenOrientation
as well as frequently used elements in the AndroidManifest.xml
:
// build.gradle
android {
defaultConfig {
applicationId "example.manifest.app"
}
buildTypes {
release {
manifestPlaceholders.screenOrientation = "portrait"
}
debug {
manifestPlaceholders.screenOrientation = "unspecified"
}
}
}
Which can be used in your Manifest as below:
// AndroidManifest.xml
<intent-filter ... >
<action android:name="${applicationId}" />
</intent-filter>
<uses-sdk tools:overrideLibrary="${applicationId}.home, ${applicationId}.util" />
<activity
android:name=".MainActivity"
android:screenOrientation="${screenOrientation}" />
Alternatively, you can specify in the form of key-value pairs at once by assigning manifestPlaceholders
as an array like this:
// build.gradle
android {
defaultConfig {
manifestPlaceholders = [baseUrl: "dev.demoserver.com",
screenOrientation: "portrait",
]
}
}
Managing Multiple Manifests
While managing separate Manifest files for each build variant sounds easy, it can get cumbersome as the build variants and flavors grow in your Android app.
For example, your app
module has multiple flavors that are needed for different build variants. The app.gradle
file will look like this:
// build.gradle
android {
buildTypes {
release { ... }
debug { ... }
}
flavorDimensions "ui", "paidui", "trialui", "referralui"
productFlavors {
paid {
dimension "ui"
}
free {
dimension "paidui"
}
trial {
dimension "trialui"
}
referral {
dimension "referralui"
}
}
}
Here, ui
and proui
are flavor dimensions which are respectively assigned to flavors paid
and free
. This allows you to generate and assign separate source-set configurations for your app, including separate Manifest files. In a verbose manner, this would result in the below files being generated:
app/src/paidDebug/AndroidManifest.xml
app/src/paidRelease/AndroidManifest.xml
app/src/freeDebug/AndroidManifest.xml
app/src/freeRelease/AndroidManifest.xml
app/src/trialDebug/AndroidManifest.xml
app/src/trialRelease/AndroidManifest.xml
app/src/referralDebug/AndroidManifest.xml
app/src/referralRelease/AndroidManifest.xml
While using multiple Manifest files sounds like an idea that is simple, it has its own problems:
Management of multiple Manifest files gets trickier with the growing number of flavors
The code becomes error-prone since the complexity and number of iterations increase with each file and flavor
Updating any configuration in Manifest requires replication in all the remaining Manifest files, which can be troublesome
File comparison, error identification, Manifest merging, and differentiating files from each other becomes extremely difficult
To solve this, you can inject variables into your AndroidManifest.xml
file, which will make sure that you have managed only one file for all your configurations and flavors specified in Gradle.
Once AndroidManifest.xml
files are ready, the Android toolchain uses Manifest merge policies to merge all the manifest files into a single source. Let us understand how the Android Manifest merging process works.
Merging Multiple Manifests
Android toolchain includes a Manifest merging tool that matches elements between multiple Manifest files logically and takes the necessary steps to generate a final merged Manifest file. This tool matches elements with two different approaches:
Key element matching, e.g. "
android:name
" declared inside<action>
tagXML tag matching, e.g.
<supports-screen>
tag
Understanding the merging process becomes crucial since multiple files can introduce conflicts and errors. You can check the merged Manifest file preview in Android Studio by navigating to AndroidManifest.xml
file and then Merged Manifest Viewer at the bottom of the editor.
The Merged Manifest view shows you the final resulting output of the merged Manifest files on the left, whereas the information for specific details and sources is on the right. Different priorities are set to different Manifest files, which are denoted by different colors on the left, and a color legend is also provided on the right.
When any two or more Manifest files include the same XML element, then the merging tool performs merging based on three predefined merge policies:
Merge
This is the standard strategy, which combines all non-conflicting elements and properties in the same tag. In the next step, child elements are merged according to their respective merge policy - if any elements or properties conflict with each other, then merge rule markers are used to merge such elements or properties together.
Merge Children Only
In this policy, the merge tool gives precedence to the attributes and tags of parent elements to the highest priority manifest, while the child elements from all the Manifest files are merged according to the merge policy.
Keep
This policy applies to all the children, keeping all the elements as is and adding them to the parent element with the same declaration in the merged Manifest file. This is a relatively less used strategy as it only allows elements with multiple declarations to be accepted for the same parent.
Manifest Merge Conflicts and Resolution
If conflict(s) appear while merging multiple Manifest files, they are displayed under the Merging Errors section on the right side of the editor. This section also helps you with hints or recommendations on how to resolve the conflict using the merge rules we discussed above.
For advanced-level configuration and Manifest merging, it becomes critical to ensure that the correct elements are merged when the final merged result of AndroidManifest.xml
is generated. To verify the same, you can find the log file which contains all the merging steps and decision trees performed by the Android toolchain to merge multiple Manifest files for you.
You can find the log files under build/outputs/logs
directory, where the file name appears as something like manifest-merger-*buildvariant*-report.txt
. This file gets updated every time Manifest is updated, and Manifest merging takes place.
Build Your Own Manifest
You've gathered all the important pieces to understand how Android Manifest placeholders can be useful for your Android apps. Let's look at one such example below:
Support Multiple Deep Links
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<!-- custom intent filter definition for deep links -->
${deepLinks_manifestPlaceholder}
</intent-filter>
Here, ${deepLinks_manifestPlaceholder}
will be replaced with a specific deep link definition. This way, you can generate deep links to accept data and intent from multiple domains, subdomains, and schemes in order to navigate your users to a specific screen as well as perform any actions on the basis of data provided via deeplink.
Manage Your Sentry.io Configuration
When setting up your Android app, Sentry will supply you with a DSN, or Data Source Name, a unique value. It seems like a standard URL, but it's actually a representation of the Sentry SDKs' configuration. The protocol, public key, server IP, and project identity are only a few of the components.
The default DSN configuration looks like this in the Manifest:
<application>
<meta-data android:name="io.sentry.dsn" android:value="https://1093t8yhqfw8h819@x11.ingest.sentry.io/123123" />
<!-- Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
We recommend adjusting this value in production. -->
<meta-data android:name="io.sentry.traces.sample-rate" android:value="1.0" />
</application>
Here, it is important to note that, the meta-data value specified for io.sentry.traces.sample-rate
specifies the performance monitoring scale for Sentry to capture the transactions. Value 1.0 equals 100% of transactions being captured. This can vary based on different environments, such as during development v/s in production.
Also, capturing transactions requires you to declare application performance monitoring as well as Activity's instrumentation. The Activity's instrumentation is declared as below:
<meta-data android:name="io.sentry.traces.activity.enable" android:value="true" />
<meta-data android:name="io.sentry.traces.activity.auto-finish.enable" android:value="true" />
Here’s how you can replace it with Manifest placeholders to achieve seamless application performance monitoring for multiple environments:
The build.gradle
file would look like this:
android {
buildTypes {
release {
manifestPlaceholders.sentryTraceSampleRate = "0.6"
manifestPlaceholders.sentryActivityTrace = true
manifestPlaceholders.sentryAutoFinishActivityTrace = true
}
debug {
manifestPlaceholders.sentryTraceSampleRate = "1.0"
manifestPlaceholders.sentryActivityTrace = false
manifestPlaceholders.sentryAutoFinishActivityTrace = false
}
}
}
With the above build.gradle
, you can modify your AndroidManifest.xml
to support placeholders as below:
// AndroidManifest.xml
<application>
<meta-data android:name="io.sentry.dsn" android:value="https://1093t8yhqfw8h819@x11.ingest.sentry.io/123123" />
<!-- Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
We recommend adjusting this value in production. -->
<meta-data android:name="io.sentry.traces.sample-rate" android:value="${sentryTraceSampleRate}" />
<meta-data android:name="io.sentry.traces.activity.enable" android:value="${sentryActivityTrace}" />
<meta-data android:name="io.sentry.traces.activity.auto-finish.enable" android:value="${sentryAutoFinishActivityTrace}" />
</application>
This way, you can also declare properties in your build.gradle
and access them in AndroidManifest.xml
with the help of manifest placeholders.
Conclusion
AndroidManifest.xml
is an essential part of any Android application since it contains all the necessary configurations to run and navigate through your app and what kind of data, permissions, and external dependencies are allowed.
In this article, you saw how you can customize elements in the Manifest file to provide variables so that different customizations can work well alongside each other, avoiding messy code as well as avoiding managing multiple Manifest files. This versatile approach not only speeds up your development time but also gives you more control and power to manage your apps while entertaining a wide variety of customizations specific to build flavors and requirements.