As I mentioned above, I was pretty excited to hear about this new upload format known as the App Bundle. Whilst this bundle still contains our applications code and resource files, the big difference is that the responsibility of building APKs is passed onto Google Play. And from here, the new Dynamic Delivery can then be used to create optimized APKs that satisfy the requirements of a users device and deliver them at runtime for install.
But why should we consider using the Android App Bundle?
- First of all, the approach promotes a clean and separated structure to your codebase. Because of the way bundles work (and especially with dynamic delivery, which we’ll come onto later), modularization by feature will become a part of your app. This is similar to the modular approach within Instant apps or general modularize-by-feature approaches. Regardless, this helps to decouple the different parts of your app and help to make your codebase easier to work with.
- Where we previously may have been required to build multiple APKs to target different API versions, device types and so on — Android App Bundles means that we can now just upload the single artifact with all of our application resources and the tooling will take care of what needs to be built and delivered to our users. This essentially automates this process for us and means we can shift that focus onto other parts of our development process.
- Because the App Bundle will build the APK that is targeted for a specific device and its configuration, this means that the APKs delivered will generally of a smaller size. This will really depend on your application, as the main savings will be from density / locale specific resources and any other unused code. Some of these size savings by early adopters of App Bundles show some great results:
- App bundles introduces us to a new concept known as Dynamic Delivery. This allows our applications to provide new features to users and allow them to be downloaded and installed at runtime as an extension to our application. This allows us to make the initial size of our application smaller and offer these extras only to users who may actually make use of them.
- And soon, the app bundle format will support instant enable on bundles — this means that users will be able to launch our feature modules instantly without installing our application, similar to the way in which instant apps currently work.
With all of this in mind, let’s take a dive into the app bundle format and all of the concepts that surround it!
Note: To follow any of the IDE parts of this article you will need to be running Android Studio 3.2.
The App bundle format
Before we get started with diving into the app bundle, it’s important to understand the format. The app bundle is made up of a zip archive that contains a collection of files that make up the bundle. Whilst these files are ones that we will find within the APKs that we are familiar with, it serves a different purpose than that of an APK.
An APK is something that we can serve directly to our users devices, whereas on the other hand an App Bundle is a publishing format that cannot be installed onto a device on it’s own. Whilst they do have their similarities, the app bundle does contain some content which we will not find within our APKs. For example, the meta data files within the bundles are used by tooling to build the APKs that will be served to our users — these files are then not included in the APKs themselves. Whilst most of the content within the app bundle you will likely already be familiar with, let’s take a look at what a typical app bundle might contain:
- manifest.xml — Just like you will find in your APKs, there is a manifest file present per app bundle. However, unlike the APK these are not stored in a binary format — it is compiled into protocol buffer format which makes it easier for tooling to transform when required.
- res / assets / libs — All resources, assets and native libraries are stored the same way as in an APK. The only difference here is that any resource files will also be stored in the protocol buffer format as the last point.
- resources.pb — Similar to the resource.arac file which can be found inside of our current APK files, the resources.pb file is a resource table which states the resources and specifics for targeting which are present inside of our app. The .pb extension is due to the protocol buffer format which is used by tooling to transform the app bundle before it is converted into the binary format used within the APK.
- assets.pb — This is the equivalent of a resource table for application assets and will only be present if you are using assets in your application.
- native.pb — This is the equivalent of a resource table for native libraries and will only be present if you are using native libraries in your application.
The last 3 files mentioned here are a key part of the App Bundle, as these tables are used to describe the targeting of our application. This is a key concept within dynamic delivery as it used to depict what kind of device and / or user that we are serving, which allows us to deliver a specific build of resources based off of this information.
These resource, assets and native tables all use information that we are already familiar with in our applications to serve content to our users. We are familiar with using directories such as drawable-hdpi, lib/armeabi-v7a, or values-es to target specific users with certain resources — the app bundle uses the exact same approach when it comes to targeting specific resources to users and devices. So when it comes to the organisation of these things we are not required to do anything differently.
Splitting up APKs
In Android lollipop we saw a feature added to the platform called Split APKs. This allowed multiple APKs to be added to a device, whilst still behaving as though they were part of a single app. These could be installed as different combinations on different devices — whilst still appearing as a single APK.
These split APKs have the exact same format as an APK, as well as sharing the same package name and version code as one another. The App Bundle format is used to generate these split APKs which in turn can be served to our users devices. To begin with the App Bundle is used to analyse all of its resources to find the parts that are common to all device configurations — this will be the manifest file, dex files and any other parts which are the same regardless of the device, architecture or locale in use. These common parts are what will make up the base APK for our application and from there, split APKs will then be used to create splits for the different kind of configurations which are possible.
From here, configuration splits will then be generated to be able to serve users the collection of split APKs that satisfy the requirements of their device setup:
- To begin with, each supported screen density will be analysed and a split APK will be created for each one, these will contain all of the resources which are specific to that density.
- A different split APK will then be generated for each native architecture
- Finally, a different split APK will be generated for each support locale for your application
So when a user is served with our application, this subset of split APKs will be delivered to their device. This is much more efficient than the current configuration of delivering every single configuration to every user, when chances are most users will not use a lot of the resources which are being delivered to their device currently. To put this into perspective, below shows three different configurations which could be served to three different users — providing only the resources that they require:
If the user changes their device configuration at any time (such as adding another choice of language), then the play store will recognize this and attempt to download the new configuration splits for all applications that use split APKs on their device. And if the device isn’t online at that time, then this will be done so at the next opportunity.
Now seeing as split APKs are only supported on Lollipop and above, the app bundle can still help to achieve size savings for devices that are using earlier versions of Android than this. Instead of their being splits generated, standalone APKs will be created that match the matrix of different combinations of architectures and device densities. In this approach all languages are included in each APK because the matrix would become too large from too many different combinations being available. For these pre-L devices, the most suitable APK will be chosen for a given device and served to the user.
As you can see from the App Bundle so far, regardless of the SDK version that we are supporting we can achieve savings when it comes to our application size. The great thing here is that as developers we don’t need to worry about any of the details that are involved with this process. We just need to upload a single app bundle and the Google Play will generate the right APK splits, followed by selecting the right APKs to be served to each device.
Building and Distributing the Android App Bundle
When it comes to building an app bundle from our project, we can do so directly from Android Studio. This can be done directly from the build menu, selecting Generate Signed Bundle / APK — from here you will be presented with the following dialog:
At this point we will be given the choice to build either an Android App Bundle or an APK. Selecting either of these options will take us to the keystore selection/creation dialog, and then from there the wizard will build our desired selection.
If we build an app bundle here then an .aab file will be generate, this is the format that represents the app bundle. As well as building an App Bundle using this wizard from within the IDE, we can also create an app bundle from the command line — this can be useful for CIs or people who just prefer working from the command line.
./gradlew modulename:assemble
./gradlew modulename:assembleVariant
When it comes to building your bundle, by default all splits will be generated. but within the android block of your build.gradle file you are able to declare which splits will be generated:
bundle {
language {
enableSplit = false
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
By default these properties will be set to true. However, setting one to false will mean that the configuration for the specified bundle will not be supported, causing the resources for that property will be packaged into the base APK and any dynamic-feature APK that is served.
Once you have built your App Bundle it can simply be uploaded to the play console. In-order to be able to distribute app bundles via the Play Store you first need to be enrolled for Google Play app signing . Because the tooling will generate the different APK splits for you, it also needs the capability of signing them — this is is required and you won’t be able to use app bundles without it.
Now that we’ve built our app bundle via Android Studio (or the command line, even), we can upload it to the Play Console to prepare for distribution. If you head on over to the release page just like you would do for an APK, you’ll notice that you’ll also be able to upload an App Bundle via the same upload area:
Once the bundle has been uploaded you will be able to see the Android App Bundle added to the list of components beneath the upload area. Expanding the Android App Bundle that we just uploaded will show you all of the information that an APK does, except this time we can see a button for Explore App Bundle:
The Application that I’ve uploaded a bundle for only supports a single locale (it’s just for example sake), but does support a range of different screen densities. Because of this, the App Bundle Explorer shows us a breakdown of the different device configuration APKs that will be served from the given app bundle. From here we will also be able to download the different APKs for testing purposes, as well as view the devices which will be served each APK configuration.
You’ll notice that at the top we are also shown a size saving value from using an App Bundle format over the standard APK upload approach. This will vary between applications and because this is just a sample app, the savings aren’t likely to be a true representative of what you may see for your own applications.
Bundle tool
Now, before you upload the App Bundle to the Google Play Store it’s important to perform some testing of the bundle. Whilst we can use an internal test track to do this, we can also test it all locally to ensure that everything is working as intended. For this we can use the bundletool which will make this process very simple for us. This tool is the same used by our IDE and Google Play to build our bundle as well as convert it to the different configuration splits, so what we will see locally is a true representation of what users will be served.
When we run the bundle tool, it will generate a collection of APKs based on the configurations of our app. To kick things off, let’s create a set of unsigned APKs for all of the different configurations that our bundle supports:
bundletool build-apks --bundle=/Users/joebirch/releases/sample.aab --output=/Users/joebirch/releases/sample.apks
Note: If you wish to run the same task but produce signed APKs, then you can do so by adding the keystore information to the command:
bundletool build-apks --bundle=/Users/joebirch/releases/sample.aab --output=/Users/joebirch/releases/sample.apks --ks=/Users/joebirch/some_keystore.jks --ks-pass=file:/Users/joebirch/some_keystore.pwd --ks-key-alias=SomeAlias --key-pass=file:/Users/joebirch/some_key.pwd
So now that we have these APKs generated, we want to serve them to a local device — bundletool can do this for us.
bundletool install-apks --apks=/Users/joebirch/releases/sample.apks
Say we have a device connected that is running at least Android 5.0. When we run this command, Bundletool will push the base APK along with any dynamic feature and configuration APKs to the device that are specific for that device configuration— the same way which users will be served our application from the Play Store. If we connect a different device, say with a different density / locale, then a different set of configuration APKs will be served to that device. If the connected device is running under Android 5.0 then the most suitable multi-APK will be installed to the device. When dealing with multiple devices you can use the --device-id=serial-id
in order to depict which device the application should be installed to.
When bundletool generates the installed content for a connected device you are able to retrieve the device specification JSON format. This can then be used to extract a specific APK from the generated APKS. To begin with we need to run the command:
bundletool get-device-spec
Now that we have this JSON file for the specific device configuration we can go ahead an use to extra a specific configuration split:
bundletool extract-apks --apks=/Users/joebirch/releases/someApkSet.apks --output-dir=/Users/joebirch/releases/device_APK_set.apks --device-spec=/Users/joebirch/releases/some_configuraton.json
You can also manually create your own device configuration JSON file and use that to extract an APK for a specific configuration. This can be great for testing a device configuration that you may not have access to specifically.
{
"supportedAbis": ["arm64-v8a"],
"supportedLocales": ["en", "es"],
"screenDensity": 640,
"sdkVersion": 21
}
Dynamically serving features
Another key part when it comes to the App Bundle is what’s known as Dynamic Delivery. This functionality allows you define modules which may not be required when an application is first installed. In our project we can define these modules and then use the new Play Core Library to install on demand when they are required. This may be a piece of functionality that not all users of your application may use, or may not be a core piece of functionality. Again, this allows size savings from initial downloads and Google Play can serve the Dynamic Feature for us when they are requested.
To be able to add support for this, let’s take a quick run through at creating a Dynamic Feature Module in our application. We can begin by heading over to the Create New Module wizard by right clicking on the root module of our project and selecting New Module. From here we can select the Dynamic Feature Module and hit Next.
Now we need to select the Base module of our application — this will be the installable module that this Dynamic Feature Module will depend on. Once the desired information has been filled out we will be prompted to name out module, and from there we can finish the setup of the Dynamic Feature module where it will be added to our application.
Once you’ve created this module, it’s worth taking a quick look around. This is so that you are aware of the difference in configuration for a Dynamic Feature Module, it’s always great to know how things work and also, if you need to convert modules in future then you know what information needs to be added.
If you open the module build.gradle file, you’ll notice that a different plugin if being used:
apply plugin: 'com.android.dynamic-feature'
Slightly different from the feature module that we may see being used for instant apps, this plugin is required for your module to be classed as a dynamic feature module. Whilst in this file you should be aware that there are a few properties which a dynamic-feature build.gradle file does not use — for example the versionCode, vesionName, minification and signing properties are all taken from the base module.
Next, if you open up your base modules build.gradle file you’ll notice that our dynamic feature has been added to an array of a dynamicFeatures property:
dynamicFeatures = [":first-dynamic-feature"]
This is declares the dynamic features which are available for your application and must be updated whenever you add support for new dynamic features so that your base module is aware of them. Finally, if you open up the dynamic feature module you can open up the manifest file of that module to find the following:
<manifest xmlns:dist="http://schemas.android.com/apk/distribution" package="co.joebirch.first_dynamic_feature"> <dist:module dist:onDemand="true" dist:title="@string/title_first_dynamic_feature"> <dist:fusing include="true" /> </dist:module>
</manifest>
- onDemand — If the property is set to true then the module will be available for on-demand downloads. When set to false the dynamic feature will be downloaded when the user first downloads and installs the application.
- title — The title will be used to identify the module when users are confirming the download of the module. The string resources for this should be stored in the resources of your application so that it can be translatable for the different locales that your application supports.
- fusing include — Setting this property to true will mean that older devices (4.4 and lower) will be able to recieve these dynamic features in the form of multi-APKs. To use this functionality you must have enabled the onDemand property.
Serving features dynamically
Now that we’ve added a Dynamic Feature Module to our application, we want to actually serve it to our users when they have requested it. For this purpose, we’re going to make use of the Play Core library which provides us with the functionality to do so. This library allows the user to intend on interacting with a feature that may not be installed yet, and at this point our application will request the feature, download it and then handle the state of this once it is installed.
To be able to start making use of this functionality, we need to begin by adding the play core library to our project via the dependency:
implementation 'com.google.android.play:core:1.3.4'
Now that we have the library available for use, we need to make use of it where appropriate. To download the dynamic feature at runtime we’ll be using the SplitInstallManager class. Whilst our application is in the foreground, this will be used to request the dynamic feature and then download it for install.
val splitInstallManager = SplitInstallManagerFactory.create(this)
Now that we have this class available, we’re going to create a SplitInstallRequest instance for the downloading of our module:
val request = SplitInstallRequest .newBuilder() .addModule("someDynamicModule") .build()
This instance will contain the request information that will be used to request our dynamic feature module from Google Play. Within this request we can state multiple modules that we want to be request, this can be done by simply chaining on multiple addModule() calls to the request builder.
Finally, we’re going to use our install manager instance to run the request that we just created. Here we will use the startInstall() function on our manager, passing in the request that we previously created, and add callbacks for when the install completes, is successful or fails so that the UI can be handled accordingly.
splitInstallManager .startInstall(request) .addOnSuccessListener { } .addOnFailureListener { } .addOnCompleteListener { }
The stateInstall function call here will trigger the install process as soon as possible. However, if you wish to defer the install process for when the app has been backgrounded then you can do so by using the deferInstall() call instead.
When you call either of these functions you will be returned with an Int value that represented the session ID for the split install. If at any point during the request for an install you want to cancel it, then you can do so by calling the cancelInstall() function, passing in the session ID for the corresponding request.
During the install process there are a collection of different errors that may occur, let’s take a quick look at what these can be:
- ACCESS_DENIED — Given the current device circumstances, the current download is not allowed
- ACTIVE_SESSIONS_LIMIT_EXCEEDED — There are too many running sessions for the current app
- API_NOT_AVAILABLE — The split install API is currently not available
- INCOMPATIBLE_WITH_EXISTING_SESSION — The session that was requested contains modules for an existing sessions as well as new modules
- INTERNAL_ERROR — There was an error when trying to process the install of the split APK
- INVALID_REQUEST — The request that was performed is invalid
- MODULE_UNAVAILABLE — The module that was requested is currently unavailable
- NETWORK_ERROR — There was a network error when trying to obtain the details for the given split
- NO_ERROR — There was no available error
- SERVICE_DIED — The service that was handling the split install has died
- SESSION_NOT_FOUND — The session that was requested couldn’t be found
Now, when this request is taking place there is no form of UI overlay that we may be used to from things such as the Billing Library or other Google Play integrations. Because of this, it’s important that the user is aware what is happening in your application when a dynamic feature is being downloaded and installed — for this we can make use of the SplitInstallStateUpdatedListener which will allow us to monitor the state of our request.
val stateListener = SplitInstallStateUpdatedListener { state -> when (state.status()) { PENDING -> { } DOWNLOADING -> { } DOWNLOADED -> { } INSTALLED -> { } INSTALLING -> { } REQUIRES_USER_CONFIRMATION -> { } FAILED -> { } CANCELING -> { } CANCELED -> { } } } splitInstallManager.registerListener(stateListener)
Here you can see an instance of the listener being defined, along with the different states which the install can be in — you should use this in your app to handle the UI. For example, you may want to show some form of progress bar to let the user know that these states are taking place, but change the message used as the process propagates through each state.
There are a collection of different states that the split install can be in, let’s take a quick look at what these are:
- CANCELED — The downloading of the split APK has been canceled
- CANCELING — The downloading of the split APK is currently being canceled
- DOWNLOADED — The split APK has been downloaded but not currently installed
- DOWNLOADING — The split APK is currently being downloaded
- FAILED — The downloading or installation of the split APK has failed
- INSTALLED — The installation of the split APK has been completed and is available for use with the application
- INSTALLING — The split APK is currently being installed
- PENDING — The download of the split APK is currently pending
- REQUIRES_USER_CONFIRMATION — The download of the split APK requires user confirmation as the size of it is too large
- UNKNOWN — The current state is unknown
Once dynamic feature modules have been installed, there are still some operations that can be performed to manage them. For example, we can uninstall a module using the deferredUninstall() function — passing in a slight of the module names which we want to remove from the users installation of our application.
splitInstallManager .deferredUninstall(listOf("someDynamicModule")) .addOnSuccessListener { } .addOnFailureListener { } .addOnCompleteListener { }
We can also retrieve a list of the installed modules names by using the getInstalledModules() function on our manager instance, this will be useful for setting up any UI in your application and checking for module install state before performing any delete requests.
val installedModules = splitInstallManager.installedModules
How much size will I actually save?
Now it’s all well and good saying that you will save application size, but it’s helpful to have some kind of guideline as to what you will actually save. According to Google, on average applications using the App Bundle format are 20% smaller in size — this means that every time your application is downloaded or updated there is 20% less data transfer involved.
Google also performed some analysis on all applications in the Play Store that had at least 1 million downloads, from this they were able to find that:
- Language splits would achieve over 95% savings when it comes resources used by locale resources
- Density splits would help to achieve up to 45% savings in applications that support multiple densities
- Applications using native libraries would be able to achieve up to 20% savings when it comes to architecture supports
With all of these put together, it was found that 10PB of data per day would be saved if all of these apps were using the App Bundle format. That is an incredibly high amount of data!
There are also some sample pieces of data that Google have shared from applications who were early adopters of the app bundle format. For example, whilst Twitter for Android already previously served multi-APKs, app bundles allowed them to see a decrease in size of around 20%. The application supports many different languages and densities which is where a lot of their savings come from. The use of the App Bundle also means that they no longer need to manually create and upload separate APKs for the configurations which they wish to support, as the App Bundle tooling will handle this automatically.
The Text Plus application on the other hand were not supporting multiple APKs for different configurations. The application has a lot of resources when it comes to different density and architectures, so the App Bundle allowed them to achieve a saving of approximately 26% for their application.
And finally, the Jamo application was able to half it’s original application size when the team added support for the App Bundle. The application uses a lot of different large native libraries to be able to support different architectures — the app bundle now allows them to optimise these requirements so that smaller APKs can be served to users.
I hope from reading this article you have some understanding of what App Bundles are, how they work and how we can pair this with Dynamic Delivery to improve how we serve our application to our users. I’m excited to use App Bundles in production and learn more about how we can use them to improve our application size and delivery. Are you using app bundles or do you have any questions? Feel free to leave a response or get in touch 🙂