Photo by Lily Banse on Unsplash

Android Project Setup — Part 2 — Fastlane Integration

Building apps @ scale

Hello again,

This is the second part (part1 here) of the Android Project Setup series where we are going to setup Fastlane for our project. The code for the project can be found here.

So in this part of the series we will be covering the following:

  • Versioning automation
  • Signing Configuration
  • Fabric Beta Distribution
  • Fastlane integration

Lets get our hands dirty!

Versioning

Android versioning, works by defining the version of your app in your gradle configuration file, like this:

versionCode 1
versionName "1.0"

Every time you want to publish a new version of your app, you have to open this file and:

  • increment the versionCode
  • (optionally) modify the versionName

Well we can achieve so with a new gradle task. But first lets agree on a versioning scheme. In my opinion{major}.{minor}.{fix} is sufficient as a scheme for most apps so this app will use that. Lets define a gradle function that reads the version from a file in our projects root directory. Add this at your app’s gradle file. If the file does not exist, will default to version 1.0.0 build 0.

def readVersion() {
def versionFile = new File(project.rootDir, 'version.properties')
def version = new Properties()
def stream
try {
stream = new FileInputStream(versionFile)
version.load(stream)
} catch (FileNotFoundException ignore) {
} finally {
if (stream != null) stream.close()
}
if (!version['major']) version['major'] = "1"
if (!version['minor']) version['minor'] = "0"
if (!version['fix']) version['fix'] = "0"
if (!version['code']) version['code'] = "0"
return version
}

Next, we need to extract the our versionName and versionCode properties from this file. This can be done by adding the following code in our gradle file.

def readVersionName() {
def version = readVersion()
return "${version['major']}.${version['minor']}.${version['fix']}"
}

def readVersionCode() {
def version = readVersion()
def code = version['code'] as int
return code
}

Now, we can use those two functions to provide us with the desired values by replacing the versionCode and versionName in our build file.

versionCode readVersionCode()
versionName readVersionName()

The following function increments the version we want (major, minor or fix) and the code. In addition it saves it in our version.properties which is located in our project’s root folder.

def incrementVersionNumber(def type = "code") {
def versionFile = new File(project.rootDir, 'version.properties')

def version = readVersion()

def major = version['major'] as int
def minor = version['minor'] as int
def fix = version['fix'] as int
def code = version['code'] as int

if (type == 'major') {
major++
minor = 0
fix = 0
} else if (type == 'minor') {
minor++
fix = 0
}else if (type == 'fix') {
fix++
}

code++

version['major'] = major.toString()
version['minor'] = minor.toString()
version['fix'] = fix.toString()
version['code'] = code.toString()

def stream = new FileOutputStream(versionFile)
try {
version.store(stream, null)
} finally {
stream.close()
}

println "Build number is now..." + build

return major + "." + minor
}

So now whats left is defining the gradle tasks that will use the incrementVersionNumber function to increase the build number. Add the following tasks at the end of your build file.

task doMajorVersionIncrement {
doLast {
println "Incrementing major version..."
incrementVersionNumber('major')
}
}

task doMinorVersionIncrement {
doLast {
println "Incrementing minor version..."
incrementVersionNumber('minor')
}
}

task doFixVersionIncrement {
doLast {
println "Incrementing fix version..."
incrementVersionNumber('fix')
}
}

task doBuildNumberIncrement {
doLast {
println "Incrementing build number..."
incrementVersionNumber('code')
}
}

You can use any of these tasks as you would use any other gradle task. Just type any of the following in your console.

./gradlew doMajorVersionIncrement
./gradlew doMinorVersionIncrement
./gradlew doFixVersionIncrement
./gradlew doBuildNumberIncrement

The output should be.

Signing Configuration

Before we distribute our app to our testers via fabric, we should sign it. If you wish to learn more about application signing check this. In order to sign your app, you need a public key certificate.

  1. Create a new directory inside your application
  2. Run keytool.exe -genkey -v -keystore beta.keystore -alias beta -clearkeyalg RSA -keysize 2048 -validity 10000 on your console inside the directory to create the cert. Store the info you provided on the keygen form somewhere safe.
  3. Create a new file in your app’s module. I like to use {config}.signing.properties, so I will name mine beta.signing.properties. Add the following content inside your configuration. Add this file to your .gitignore.
storeFile=keystores/beta.keystore
storePassword=storePassword
keyAlias=beta
keyPassword=keyPassword

4. Modify your build gradle a bit and add the following

android {
compileSdkVersion 28

signingConfigs {

beta {
def properties = getSigningProperties('beta.signing.properties')
keyAlias properties['keyAlias']
keyPassword properties['keyPassword']
storeFile file(properties['storeFile'])
storePassword properties['storePassword']
}

}

...

// Defines the environment we are working on
flavorDimensions "environment"

productFlavors {
stage {
dimension "environment"
applicationIdSuffix ".stage"
versionNameSuffix "-stage"
}

production {
dimension "environment"
}
}

buildTypes {

debug {
applicationIdSuffix ".debug"
debuggable true
}

release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
productFlavors.stage.signingConfig signingConfigs.beta
productFlavors.production.signingConfig signingConfigs.beta
}
}

}
dependencies {
...
}
def getSigningProperties(def filepath) {
if (!filepath) {
throw new GradleException("Signing properties filepath cannot be empty.")
}
def keystorePropertiesFile = project.file(filepath)
def keystoreProperties = new Properties()
def stream
try {
stream = new FileInputStream(keystorePropertiesFile)
keystoreProperties.load(stream)
} catch(FileNotFoundException ignore) {
throw new GradleException("Signing properties file not found.")
}
assert keystoreProperties['keyAlias'] != null
assert keystoreProperties['keyPassword'] != null
assert keystoreProperties['storeFile'] != null
assert keystoreProperties['storePassword'] != null

return keystoreProperties
}

5. Run ./gradlew assembleStageRelease and you should find your app-stage-release-apk in {app_module}/build/outputs/apk/stage/release/ folder.

Fabric Beta Distribution

Now we need to install Fabric. Follow the documentation and you will be ready to rumble in a few minutes. We will use Fabric’s Beta & Crashlytics for this setup.

Beta is a nice tool to distribute your app. It will migrate to Google’s Firebase platform by mid 2019, but there it is not available atm.

Crashlytics is the crash reporting tool powered by fabric. It has already migrated to Firebase.

By integrating fabric into our project we gain access to a new gradle task that allows us to upload an apk to the Beta distribution server and deliver it to our testers. By typing./gradlew assembleStageRelease crashlyticsUploadDistributionStageRelease, you apk will be uploaded into beta as well. You can also drag & drop you built apk into fabric plugin’s ui, but thats just lame…

Fastlane (for Mac OS)

Fastlane is an automation tool for mobile development tasks. As mentioned on its website fastlane handles tedious tasks so you don’t have to.. We will cover the beta deployment step. It is unfortunate that fastlane is currently available for Mac OS users only. We tried a bit to make it work on Windows as well, but too many errors came up and we wont spend more time for now.

This isn’t a Fastlane installation tutorial so I will skip the details of this task and get straight to the point. We will write a lane that updates our version, builds our project variant, uploads it to Beta and commits our changes. Its just an example and you can always modify it for your project’s needs.

What we will achieve via this is a cli that

  • updates our version
  • builds the app
  • uploads it to beta
  • commits the version.properties to the git

The following code defines a lane. The options variable will be holding our command line options passed to our lane.

lane :beta do |options|
end

First thing I want to do is update the version of our project, so lets add our gradle, but i would also like to explicitly define which part of the version I want to update. The following code does exactly that.

if (options[:versionChange]=='minor')
gradle(task: 'doBuildNumberIncrement')
gradle(task: 'doMinorVersionIncrement')
elsif (options[:versionChange]=='fix')
gradle(task: 'doBuildNumberIncrement')
gradle(task: 'doFixVersionIncrement')
elsif (options[:versionChange]=='major')
gradle(task: 'doBuildNumberIncrement')
gradle(task: 'doMajorVersionIncrement')
else
gradle(task: 'doBuildNumberIncrement')
end

So if we run fastlane beta versionChange:minor, the version.properties file of our project will have its fix and build version number incremented.

Next step is to build our app. The following line will build our stageRelease variant.

gradle(task: ‘assemble’, build_type: ‘release’, flavor: ‘stage’)

Next, we need to upload our apk to the beta channel. Don’t forget to set the Crashlytics api token and build secret environmental variables.

crashlytics(
api_token: ENV["CRASHLYTICS_API_TOKEN"],
build_secret: ENV["CRASHLYTICS_BUILD_SECRET"],
groups: "developers",
notes: changelog_from_git_commits()
)

Last but not least, it would be nice to have our version.properties file committed and pushed to our develop branch. So add the following lines in your lane.

git_commit(path: "version.properties", message: "chore: Update version.properties")
push_to_git_remote(
local_branch: "develop",
remote_branch: "develop",
tags: false
)

Summing up, we now have

  • An automatic versioning system
  • Bundle signing for our stageRelease variant
  • A distribution mechanism for our beta testing
  • A tool to automate and orchestrate all these tasks