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
A new edition of this article is now featured in the React Native DevOps Guide. Take a look for my latest recommendations!
Check out Running Android Builds, Part 4!
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
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.gradleandroid {
...
dexOptions {
preDexLibraries false
javaMaxHeapSize "8g"
}
...
}// android/gradle.propertiesandroid.useDeprecatedNdk=trueorg.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.propertiesNODE_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.gradlebuildTypes {
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.gradledef 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.gradleproject.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.gradleproject.ext.react = [
entryFile: "index.js",
nodeExecutableAndArgs: hasProperty(’NODE_PATH’)?[NODE_PATH]: ["node"],
bundleInDebug: false,
bundleInStaging: true,
bundleInRelease: 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/Fastfilebefore_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
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/Fastfileif 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
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...# Productionif 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