Android + React Native + Fastlane: Working with multiple build types

TLDR: add staging build configuration, add applicationIdSuffix to build variants, improve Gradle VM performance, add react ExtraPropertiesExtension parameters, fix node executable + VM issues, solve BundleJSAndAssets hang, fastlane automation

Our team recently released the Android version of our React Native app. Both iOS / Android build and deployment flows have their challenges, and I want to share what we learned through the process of bringing the Android app to the Play Store.

What problems did we run into?

  • Java VM Performance needed improvement
  • Unclear what node binary JVM was using
  • React Native packager was not bundling JS+Assets in the staging variant
  • React Native packager was hanging on BundleJSAndAssets build step indefinitely
  • Multiple build variants could not be loaded on to a phone / sim at the same time.
  • Wanted app icon modifications for dev and staging builds for easy identification on device

The companion repository has a sample project with my gradle configuration, as well as a Fastfile for running the three build variants. You will be able to generate app-debug, app-staging, and app-release.apk files with the example code.

Note: Staging & Release builds require you to have a properly configured keystore to sign the .apk.

Companion Repository: https://github.com/TGPSKI/androidBuildTypes

Three build variants on the same simulator

Improving Build Performance

Our first builds for Android were very slow, and we ran into memory and path issues. We solved these issues by increasing the Java VM Heap size, explicitly defining our node path (very important when using NVM to manage multiple node versions), and adjusting Gradle configurations to better suit our environment.

Increase Java VM Memory Heap Size

Give the VM a larger heap size to speed up builds. Define the max heap size in 2 locations, android/app/build.gradle and android/gradle.properties.

// android/app/build.gradle
android {
...
dexOptions {
preDexLibraries false
javaMaxHeapSize "8g"
}
...
}
// android/gradle.properties
android.useDeprecatedNdk=true
org.gradle.daemon=true
org.gradle.parallel=false
org.gradle.configureondemand=false
org.gradle.jvmargs=-Xmx8704M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

Here, org.gradle.jvmargs=-Xmx8704M matches the javaMaxHeapSize property we set above. If you use a different memory buffer size, change the parameter here to match the heap size in megabytes.

I also set org.gradle.parallel and org.gradle.configureondemand to false. This ensures proper bundling behavior during Bundle[Type]JSAndAssets . Setting the daemon flag to true will speed up future builds as well.

Explicitly define node executable path

We use NVM to ensure common node versions between our developers and CI tools. I need to be sure that node, as run by the Java VM, is using the correct executable.

Setting theNODE_PATH variable in your user-space gradle.properties file allows you to reference the NODE_PATH variable in your project-level build.gradle file.

// ~/.gradle/gradle.properties
NODE_PATH=[YOUR_HOME_HERE]/.nvm/versions/node/v6.11.3/bin/node

Configuring Multiple Build Types

By default, react-native init will generate a project with two buildTypes.

We use 3 build configurations in our development. Each has a different Codepush key, as well as variable signing configurations.

Setting an applicationIdSuffix for each build type allows multiple .apk files to coexist on the same simulator, without being overwritten. We add .debug and .staging suffixes to the build type configurations to aid development.

You can also modify buildConfigFields for each type. Setting a different codepush key for each allows us to test JS updates with different sets of users in a controlled way.

// android/app/build.gradle
buildTypes {
debug {
buildConfigField "String", "CODEPUSH_KEY",
'"DEV_KEY_HERE"'
applicationIdSuffix ".debug"
}
        staging {
buildConfigField "String", "CODEPUSH_KEY",
'"STAGING_KEY_HERE"'
...
signingConfig signingConfigs.release
applicationIdSuffix ".staging"
}
        release {
buildConfigField "String", "CODEPUSH_KEY",
'"PROD_KEY_HERE"'
...
signingConfig signingConfigs.release
}
}

Configure React ExtraPropertiesExtension Parameters

In addition to modifying the buildTypes configuration, we also have to tell react packager to set the bundle and dev mode flags for the new staging config.

Taking a look at react.gradle , dev mode and bundling behavior are set by

  • config parameters, or
  • having release in the buildType name.
// node_modules/react-native/react.gradle
def devEnabled = !(config."devDisabledIn${targetName}" ||
targetName.toLowerCase().contains("release"))
...
enabled config."bundleIn${targetName}" ||
config."bundleIn${buildTypeName.capitalize()}"
?:
targetName.toLowerCase().contains("release")

We have added the staging buildType, so we need to set config parameters that tell the packager what to do with assets and JS in this build.

Bringing the changes into the build.gradle script

We can set specific bundling and dev mode behaviors for each buildType we want to create. For staging, we want to bundle JS and disable dev mode. The main difference from a release variant is a different codepush key — giving us the ability to validate a codepush release before promoting the code to prod.

// android/app/build.gradle
project.ext.react = [
bundleInStaging: true,
devDisabledInStaging: true
];
...
apply from: "../../node_modules/react-native/react.gradle"

In addition to bundling behavior, you can define the entry file, the node executable path, packager directories, and packager input exclude directories.

Preventing hangs on bundleReleaseJSAndAssets

After building sourcemap exports into our automated build flows, we started to see infinite hangs during the bundleReleaseJSAndAssets step for Android builds.

After lots of digging through open GH issues and a bit of debugging, I realized that the new bundle export step in the build flow was confusing the packager on subsequent builds.

There are a few different types of files that can cause the packager to hang during this step. I have seen reports of Realm.js database files, Jest test artifacts, and code bundles causing issues with this step.

By adding inputExcludes paths to the build.gradle file, we can easily tell react.gradle what paths to skip when bundling.

inputExcludes: ["ios/**", "__tests__/**", "bundle_out/**]

Full project.ext.react configuration

The NODE_PATH parameter set in~/.gradle/gradle.properties is used to define the react packager node executable path. For each of the build types, I have explicitly defined the asset and code destinations.

// android/app/build.gradle
project.ext.react = [
entryFile: "index.js",
nodeExecutableAndArgs: hasProperty('NODE_PATH')?[NODE_PATH]: ["node"],
bundleInDebug: false,
bundleInStaging: true,
bundleInStaging: true,
jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
jsBundleDirStaging: "$buildDir/intermediates/assets/staging",
resourcesDirStaging:
"$buildDir/intermediates/res/merged/staging",
jsBundleDirRelease: "$buildDir/intermediates/assets/release",
resourcesDirRelease:
"$buildDir/intermediates/res/merged/release",
devDisabledInDebug: false,
devDisabledInStaging: true,
devDisabledInRelease: true,
inputExcludes: ["ios/**", "__tests__/**", "bundle_out/**]
];

Running builds with Fastlane

There is a lot of prior art on how to integrate Fastlane into a project to have predictable build and deployment behavior. I want to discuss a few customizations I’ve made to our scripts in order to further automate our deployments.

Note: If you are cloning the companion repository, you need to have fastlane installed on your system before running the commands.

Additional configuration is required before running add_badge,crashlytics ,orsupply actions in the example lanes.

Forcing Yarn, Assigning version and revision code variables

My before_all lane triggers yarn install to run before each build. This ensures that all node module versions are the same for each build and deployment action. Moving yarn install to the Fastfile eases adding mobile deployment to a CI Server (in our case, Jenkins) — this forces consistent node modules versioning across different servers and machines.

  • yarn fastlane plugin

I also use fastlane plugins to getANDROID_VERSION_NAME and..._CODE parameters from the app/build.gradle file. These are used in the Fastfile to set badge icons, auto-generate git commit and slack webhook messages, and for other purposes.

# fastlane/Fastfile
before_all do
update_fastlane
yarn(
command: "install",
package_path: "./package.json"
)
ANDROID_VERSION_NAME = get_version_name(
gradle_file_path:"android/app/build.gradle",
ext_constant_name:"versionName"
)
ANDROID_VERSION_CODE = get_version_code(
gradle_file_path:"android/app/build.gradle",
ext_constant_name:"versionCode"
)
...
end

Badges

Alpha, Staging, and Prod builds with customized Badges

The badge gem by HazAT is an easy way to add a visual build information to the app icon. The repo is slated for iOS use, but adding glob to the add_badge action lets us modify Android app icons as well. You will need to install imagemagick to support this gem.

# fastlane/Fastfile
if options[:badge]
add_badge(
shield: "#{ANDROID_VERSION_NAME}-#{ANDROID_VERSION_CODE}-
blue",
glob: "/android/app/src/main/res/mipmap-*/ic_launcher.png",
shield_scale: "0.75"
)
end
fastlane android dev badge:true

Crashlytics and Play Store Uploads

Adding deployment features to your Fastfile helps create a repeatable deployment process, as well as making integrations with CI Servers easier. I use Beta (part of Crashlytics) to distribute staging builds to Android and iOS users with the same distribution platform.

Using lane options, you can easily trigger builds that upload artifacts to various services.

# fastlane/Fastfile
# Staging
# build the release staging variant
gradle(task: "assemble", build_type: "staging", project_dir:
"./android/")
# upload to Beta by Crashlytics
if options[:CRASHLYTICS_API_KEY] &&
options[:CRASHLYTICS_BUILD_SECRET]
crashlytics(
api_token: options[:CRASHLYTICS_API_KEY],
build_secret: options[:CRASHLYTICS_BUILD_SECRET],
groups: ["your-test-groups-here"],
notes: "#{ANDROID_VERSION_NAME}-#{ANDROID_VERSION_CODE}
staging release - fastlane generated",
notifications: true
)
end
end
...
# Production
if options[:play_store]
# upload to alpha track on google play developer's console
supply(track: "alpha", apk:
"android/app/build/outputs/apk/app-release.apk")
end

Video Examples: Fastlane commands

Here are video examples of the three build types. Run from your react-native project root.

Note: Additional configuration is required before running add_badge,crashlytics ,orsupply actions in the example lanes.

  • fastlane android dev clean:true badge:true
  • fastlane android staging badge:true
  • fastlane android prod play_store

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.