GloballyDynamic: Dynamic delivery with Firebase App Distribution

Jesper Åman
6 min readAug 22, 2020

--

This is the second article in a three part article series on how you can leverage GloballyDynamic to accomplish tasks such as:

  • Enabling dynamic delivery for environments where it would be otherwise unavailable (e.g. Firebase App Distribution).
  • Testing various outlying dynamic delivery scenarios during development.
  • Making life easier when working with multiple dynamic delivery platforms (e.g. Play Feature Delivery or Dynamic Ability) for the same project.

Series outline:

Firebase App Distribution (formerly Fabric Beta) is a distribution platform that allows for quick and flexible beta testing, it is very useful for getting builds out to trusted testers early in order to receive feedback and collect statistics.

The platform does not (yet at least) support app bundles, this is a bit unfortunate if you otherwise publish your app in the form of an app bundle on Google Play Store, since the internally tested version on Firebase App Distribution will have slightly different behaviour compared to the one published on Play Store. For example, the flow for downloading and installing dynamic feature modules will not be testable on Firebase App Distribution.

While dynamic feature module flows can be tested very nicely through Internal App Sharing, it requires testers to have Google Play installed on their devices — although this condition holds true most of the time, it is becoming more common for devices to not have it, e.g. newer Huawei devices as well as Amazon Fire devices come without Play Store installed.

Dynamic delivery for platforms like Firebase App Distribution can be enabled through a process very similar to the outlined in the previous article about a development setup. If you haven’t done so already, I suggest you give it a glance before proceeding with this article.

The flow between all components involved in making this happen is illustrated in the image below:

Dynamic delivery with Firebase App Distribution

The two main differences here in comparison to the development setup described in the previous article are the following:

  • The GloballyDynamic Server obviously has to be reachable from apps that are installed via Firebase App Distribution, hence we have to host it in a dedicated location which allows for that, i.e. not on the local development machine.
  • An APK that has been stripped of on-demand dynamic features, but not stripped of install-time dynamic features, has to be produced and uploaded to Firebase App Distribution.

The remainder of this article will focus on how to get these pieces of the puzzle in place.

1) Run a GloballyDynamic Server

To be able to store bundles and send split APKs (dynamic feature APKs etc) to clients, we need to launch a GloballyDynamic Server in a location that is reachable from our app.

The server is designed to be lightweight and environment agnostic, it can be embedded into a Java application, or run standalone on any system where Java is available. Below follows a few examples on how it can be launched.

Running a standalone server

To start a server, you can simply run the following shell commands on your favourite cloud provider or a dedicated server machine on your network:

# Download the latest server jar
curl -L --output globallydynamic-server.jar https://github.com/jeppeman/GloballyDynamic/releases/download/server-1.0.0/globallydynamic-server-1.0.0-standalone.jar
# Run the server
java -jar globallydynamic-server.jar \
--port 8080 \
--username johndoe \
--password my-secret-password \
--storage-backend local \
--local-storage-path $(pwd)

Note that in the above example, uploaded bundles will be stored on the machine on which the server is running, you can configure the server to store bundles on Google Cloud Storage or Amazon s3 as well; refer to the documentation for a complete list of configurable options.

Embedding the server into an application

If you’d rather embed the server into an existing Java application (such as a Spring Boot app), it is available on maven central with the following coordinates:

com.jeppeman.globallydynamic.server:server:1.0.0

You then configure and start the server through the following API:

val configuration = GloballyDynamicServer.Configuration.builder()
.setPort(8080)
.setUsername("johndoe")
.setPassword("password")
.setStorageBackend(StorageBackend.LOCAL_DEFAULT)
.build()
val server = GloballyDynamicServer(configuration)server.start()

Running the server on Google Compute Engine

As a way of getting up and running quickly with a server that is immediately publicly reachable, I’ll also provide an example on how to launch a server on Google Compute EngineIf you choose to run the server elsewhere than Google Compute Engine you can skip to step 2.

Note: As with any cloud provider, running a server on Compute Engine incurs costs. However, new customers get $300 worth of free credit, which is plenty for evaluation purposes. You can read more on GCP free tier here.

If you don’t have it installed already, start by installing and configuring Google Cloud SDK and creating a project. Once that is done, run the following shell command:

curl https://globallydynamic.io/scripts/compute_engine_launch.sh | /usr/bin/env bash

It will download and run a script that:

A) Creates a bucket in Google Cloud Storage in which to store uploaded bundles. B) Creates a VM instance on Compute Engine and starts a GloballyDynamic server on it, the server has the same credentials configured as the above examples (johndoe/my-secret-password).

If you’re curious about the exact steps of the downloaded script you can find the source here.

On the last line of output from the script you should see a printout of what the address to the started server is, looking something like this:

Done! Address to server: http://<ip-to-server>:8080

This means that the server has started successfully and is publicly accessible. We can now use this address to configure our firebase builds to use dynamic delivery, like so:

android {
flavorDimensions 'platform'
productFlavors {
firebase {
dimension 'platform'
}
}
globallyDynamicServers {
serverUrl 'http://<ip-to-server>:8080'
username 'johndoe'
password 'my-secret-password'
applyToBuildVariants 'firebaseRelease'
}
}
dependencies {
firebaseImplementation 'com.jeppeman.globallydynamic.android:selfhosted:1.0.0'
}

2) Produce an APK suitable for dynamic delivery and upload it to Firebase App Distribution

Since we can not upload app bundles to Firebase App Distribution, we have to upload an APK, in particular an APK that does not contain on-demand dynamic feature modules, but does contain install-time dynamic feature modules.

Building the APK

We can not use ./gradlew assemble for this end since it will leave out all dynamic feature modules, on-demand and install-time alike.

Instead we have to build a universal APK; meaning an APK that has been produced from an app bundle that contains all code and resources, including that from dynamic feature modules.

A problem with universal APKs is that they also includes on-demand dynamic features by default. Luckily, there is a straightforward way to exclude them — in the AndroidManifest.xml of your on-demand module, disable fusing, this will cause it to be excluded from the universal APK, as illustrated below:

<manifest xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.myapplication.dynamicfeature">

<dist:module
dist:instant="false"
dist:title="@string/my_feature">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<!-- Fusing disabled -->
<dist:fusing dist:include="false" />
</dist:module>
</manifest>

So how do we actually build a universal APK? The Android Gradle Plugin does not currently expose a task which can, so normally you have to build a bundle with AGP (./gradlew bundle), and then use bundletool from the command line to extract a universal APK from it.

However, the GloballyDynamic Gradle plugin exposes a task that combines these steps, so you can simply run:

./gradlew buildUniversalApkForFirebaseRelease

The path to the universal APK produced by this task will be ${buildDir}/outputs/universal_apk/firebaseRelease/universal.apk.

Uploading the APK

In order to upload the universal APK, we need to instruct the Gradle plugin of Firebase App Distribution to select it, this is done as follows:

apply plugin: 'com.google.firebase.appdistribution'android {
flavorDimensions 'platform'
productFlavors {
firebase {
dimension 'platform'
firebaseAppDistribution {
apkPath "${buildDir}/outputs/universal_apk/firebaseRelease/universal.apk"
}
}
}
}

With this configuration in place, all that’s left to do is to run the upload task exposed by Firebase, like so:

./gradlew appDistributionUploadFirebaseRelease

That’s it. After that you can download the APK from Firebase App Tester app and start installing split APKs from our GloballyDynamic Server through GlobalSplitInstallManager#startInstall(GlobalSplitInstallRequest).

The gist below puts all of the above configuration together in a more complete manner:

For a live example, you can have a look at my example project which is published on Firebase App Distribution with dynamic delivery.

The method described in this article, apart from the uploading, can be used for enabling dynamic delivery with any other platform that does not support it natively, such as Amazon App Store or Samsung Galaxy Store, you can read more on this on the website.

In the third and final part of the series we’ll go through how to configure a project for handling multiple dynamic delivery platforms, e.g. Google Play Store, Huawei App Gallery and Firebase App Distribution.

Useful links

--

--