fbpx

How To Implement CI/CD For Android App Development?

Introduction

Let me start with one question, As an Android developer how much time do you waste in deploying/distributing an APK for someone to test?

Let’s cut-up the whole process into small tasks and understand it.

  • We have 3 environments: Development, Staging, Release. According to which environment build we want to generate change backend URLs, firebase files, other dependencies that vary according to the environment.
  • Compile it, Generate APK.
  • Upload APK to third-party distribution (e.g Google drive, Firebase distribution).
  • Notify testers with release notes that the build is ready for testing.

A boring tiring process that takes a lot of time and energy every single time when you want to do it. Here automation comes to make our life easier. We can automate this manual process and get rid of the time-wasting problem. By using CI/CD we can achieve this. But hey,

What Is CI/CD?

Let me tell you what CI/CD in a nutshell?

CI/CD is an abbreviation of Continuous Integration and Continuous Delivery or Continuous Deployment.

Continuous Integration(CI) is a development practice that requires developers to integrate source code into a shared repository frequently. Then each check-in(commit) is verified by an automated build which allows the team to detect the problem early and solve problems quickly.

Continuous Delivery(CD) is the ability to get changes of all types—including new features, configuration changes, bug fixes, and experiments—into production, or into the hands of users, safely and quickly in a sustainable way.

Now, allow me to share my implementation of CI/CD for android. It is composed of 4 steps,

1. Environment Setup: As I mentioned before that we have 3 different environments, to automate this we will know what changes we need to make.

2. Firebase Setup: Firebase has an app distribution service that makes distributing your apps to testers painless. You can get early feedback as your apps are getting quickly to testers’ devices.

3. Fastlane: Fastlane is an open-source platform aimed at simplifying Android and iOS deployment. Fastlane lets you automate every aspect of your development and release workflow.

4. Jenkins: It is open source and free automation server software by which we can achieve continuous integration and continuous delivery.

Jenkins CI/CD To Deploy Angular Application On Azure Storage

How To Do It?

Environment Setup:

Why do we have 3 different API environments?

Creating separate API environments for development, testing and production provide each built with its own database, code-base and other respective backend services. This allows developers to continue to work and make changes, even while the app is in testing mode, and ensures (beta) testers do not mess up the production database.

My requirement from the environment is I have different backends and use different firebase projects. Also, I want to install 3 different applications on my mobile simultaneously. So I can track the features and testers also can differentiate between different builds.

To differentiate builds we can name our application as “DEV APPNAME”, “STAG APPNAME”, “APPNAME”.

1. Create properties files

Create a new directory “Config” and inside that create 3 properties files-

  • development.properties
  • staging. properties
  • release.properties

The purpose of these files is to store constant variables that vary across different environments such as BASE_URL, S3 BUCKET_URL, AWS credentials, etc. In our case, we have different names of apps as well as different app id. We are going to store all these properties in these files.

As you can see, I have declared 3 variables in a properties file, APPNAME, APPLICATION_ID_SUFFIX, API_URL.

Now we want to access these variables in Gradle and inside code. To read this variable inside Gradle, let’s write one function which will load these variables from a properties file. Write this function at the end of the app/build.gradle

def getProps(path) {

   Properties props = new Properties()

   props.load(new FileInputStream(file(path)))

   return props

}

The above function takes the properties file path, loads and returns the properties.

Now we want to set these properties such as these variables can be read in code.

At build time, Gradle generates the BuildConfig class so your app code can inspect information about the current build. You can also add custom fields to the BuildConfig class from your Gradle build configuration file using the buildConfigField() method and access those values in your app’s runtime code.

You can read more about it here.

Now we want to set APPNAME and APPLICATION_ID_SUFFIX and CONSTANTs according to build type. So add/replace the build types block with the below code in app/build.gradle

buildTypes {

       release {

           minifyEnabled false

           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

           ext.config = getProps('../config/release.properties')

           ext.config.each { p ->

               if(p.key == "APPNAME"){

                   resValue "string","app_name", p.value

               }

               else if(p.key=="APPLICATION_ID_SUFFIX"){

                   applicationIdSuffix p.value

               }else

                   buildConfigField 'String', p.key, p.value

           }


       }

       staging {

           minifyEnabled false

           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'


           ext.config = getProps('../config/staging.properties')

           ext.config.each { p ->

               if (p.key == "APPNAME") {

                   resValue "string", "app_name", p.value.replace('"', '')

               } else if (p.key == "APPLICATION_ID_SUFFIX") {

                   applicationIdSuffix p.value

               } else {

                   buildConfigField 'String', p.key, p.value

               }

           }

       }

       debug {

           minifyEnabled false

           proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

           ext.config = getProps('../config/development.properties')

           ext.config.each { p ->

               if (p.key == "APPNAME") {

                   resValue "string", "app_name", p.value

               } else if (p.key == "APPLICATION_ID_SUFFIX") {

                   applicationIdSuffix p.value

               } else

                   buildConfigField 'String', p.key, p.value


           }

       }

   }

Read more about build types.

Now we are setting the app name from the Gradle so we have to delete the pre-created app name string variable from the app/src/main/res/values/strings.xml file.

Firebase:

To distribute the app for testing, there are multiple distribution service providers available in the market. But we are going to use the firebase app distribution service. This service lets us manage the distribution and versions of the app.

Firebase App Distribution makes distributing your apps to trusted testers painless. By getting your apps onto testers’ devices quickly, you can get feedback early and often.

Steps for firebase setup:

1. Create a firebase project

Now we have 3 different apps for 3 different environments, so create 3 projects. Make sure you enter the application id.
For development => com.myapp.dev

For staging => com.myapp.staging
For release => com.myapp

2. Enable Firebase app distribution service for all the apps

3. Copy APP id.

4. If you are using any other service of firebase such as firestore which needs googleservices.json file, then download.

In this case, we have three different googleservices.json files for different environments. To maintain this android provides a simple directory structure. The google-services.json file is generally placed in the app/ directory (at the root of the Android Studio app module). As of version 2.2.0, the plugin supports build type and product flavor specific JSON files.

// debug, staging and release are product flavors.
app/
    src/debug/google-services.json
    src/staging/google-services.json
    src/release/google-services.json

    ....

Recommended Reading:

To distribute the app, we need to authenticate and upload the build to the firebase from CI, for that firebase provides a CLI token.

1. To generate a CLI token, we need to install Firebase CLI

$ curl -sL https://firebase.tools | bash
$ firebase login: ci

It will generate a firebase token by which we can authenticate and upload the build to firebase app distribution.

Copy and save the firebase token, we will need it later in fastlane.

2. Add tester group in firebase app distribution.

3. Get app id, we will need it later

Now we are going to use Fastlane for automating the generation and distribution of app to testers. But,

What Is Fastlane?

Fastlane can be simply described as the easiest way to automate building and release your iOS and Android apps, via a suite of tools that can work either autonomously or in tandem to accomplish tasks in android such as:

  1. Generating build
  2. Sign APK using Keystore
  3. Upload to firebase distribution
  4. Notify Testers

Now we know what Fastlane is and why it is used? Let’s move on to how to use Fastlane?

Install Fastlane

There are multiple ways to install Fastlane. You can choose any one of them.

Getting started with fastlane for iOS

Once you successfully install Fastlane. We will first initialize Fastlane.

$ cd project_root
$ fastlane init

It will generate a Gemfile and Fastlane folder in the root.

It follows simple instructions defined in a Fastfile. After you set up Fastlane and your Fastfile, you can integrate App Distribution with your Fastlane configuration. The Fastfile has to be inside your ./fastlane directory.

To add App Distribution to your Fastlane configuration, run the following command from the root of your android project:

$ fastlane add_plugin firebase_app_distribution

./fastlane/Fastfile

default_platform(:android)
platform :android do
 desc "Generate build and upload to firebase"
 lane :build do
   slack_send(':crossed_fingers: Generating '+ENV['BUILD_TYPE']+' build')
   gradle(
     task: "assemble",
     build_type: ENV['BUILD_TYPE'],
     properties: {
       "android.injected.signing.store.file" => ENV['KEYSTORE_FILE'],
       "android.injected.signing.store.password" => ENV['KEYSTORE_PASS'],
       "android.injected.signing.key.alias" => ENV['KEY_ALIAS'],
       "android.injected.signing.key.password" => ENV['KEY_PASS'],
     }
   )
   slack_send(ENV['BUILD_TYPE']+' Build Successfully completed...:star-struck: \n Uploading to Firebase')
   firebase_app_distribution(
     app: ENV["FIREBASE_APP_ID"],
     release_notes_file: "releaseNotes.txt",
     groups: "Internal",
     firebase_cli_token: ENV['FIREBASE_CI_TOKEN'],
     debug: false
   )
   slack_send(':tada: Hooooooorrrayyyyy!!! '+ENV['BUILD_TYPE']+' Build is successfully uploaded on Firebase  Distribution!! :dancer::man_dancing:')
  end
end

def slack_send(msg)
    slack(
      message: msg,
      success: true,
      channel: '#'+ENV['CHANNEL'],
      default_payloads: []
    )
end

Fastlane’s build lane will compile and sign APK file with Keystore, notify status to slack and then it will upload to firebase app distribution.

Now you notice we have used the environment variable in Fastfile. You can export these variables or pass the environment variable file while running Fastlane.

Let’s add environment variable files. We will use this Fastfile for multiple environments.

Create environment variable file for development

./fastlane/.env.development

BUILD_TYPE=Debug
KEYSTORE_FILE = "CICDDemoDev.jks"
KEYSTORE_PASS = "CICDDemo@123"
KEY_ALIAS = "CICDDemo"
KEY_PASS="CICDDemo@123"

FIREBASE_APP_ID="1:304151838b333a0491c"
FIREBASE_CI_TOKEN="1//0gPzWx8kEFjgKAOjSnvtU1VbDIxLq78"
CHANNEL="jenkins"
SLACK_URL="https://hooks.slack.com/services/T04PdMmDa3s5"

We will have the same variables for staging and release. Only build type, keystore and Firebase app id variables will be changed for different environments.

Firebase token, channel name and slack URL will be the same.

./fastlane/.env.staging

BUILD_TYPE=Staging
KEYSTORE_FILE = "CICDDemoStag.jks"
KEYSTORE_PASS = "CICDDemo@123"
KEY_ALIAS = "CICDDemo"
KEY_PASS="CICDDemo@123"

FIREBASE_APP_ID="1:8b333a0491c30415183"
FIREBASE_CI_TOKEN="1//0gPzWx8kEFjgKAOjSnvtU1VbDIxLq78"
CHANNEL="jenkins"
SLACK_URL="https://hooks.slack.com/services/T04PdMmDa3s5"

./fastlane/.env.master

BUILD_TYPE=Release
KEYSTORE_FILE = "CICDDemo.jks"
KEYSTORE_PASS = "CICDDemo@123"
KEY_ALIAS = "CICDDemo"
KEY_PASS="CICDDemo@123"

FIREBASE_APP_ID="1:151838b333a0491c304"
FIREBASE_CI_TOKEN="1//0gPzWx8kEFjgKAOjSnvtU1VbDIxLq78"
CHANNEL="jenkins"
SLACK_URL="https://hooks.slack.com/services/T04PdMmDa3s5"

You can run this fastlane build using,

$ fastlane android build --env <env-file>
Eg:
$ fastlane android build --env development

What Is Jenkins? How We Can Use It?

Jenkins® is is a free and open-source automation server. With Jenkins, organizations can accelerate the software development process by automating it. Jenkins manages and controls software delivery processes throughout the entire lifecycle, including build, document, test, package, stage, deployment, static code analysis and much more.

You can set up Jenkins to watch for any code changes in places like GitHub, Bitbucket or GitLab and automatically do a build a with tools like Maven and Gradle. You can utilize container technology such as Docker and Kubernetes, initiate tests and then take actions like rolling back or rolling forward in production.

What Is The Jenkins Pipeline?

Jenkins Pipeline (or simply “Pipeline”) is a suite of plugins that supports implementing and integrating continuous delivery pipelines into Jenkins.

A continuous delivery pipeline is an automated expression of your process for getting software from version control right through to your users and customers.

We can write one text file in which we define multiple stages of the process which in turn can be committed to a project’s source control repository. This is the foundation of “Pipeline-as-code”; treating the CD pipeline a part of the application to be versioned and reviewed like any other code.

A Jenkinsfile can be written using two types of syntax

  • Declarative
  • Scripted

Declarative Pipeline is a more recent feature of Jenkins Pipeline which:

  • Provides richer syntactical features over Scripted Pipeline syntax, and is designed to make writing and reading Pipeline code easier

To generate an android build we need to install android SDK, Fastlane on the Jenkins agent. To avoid that, we will use docker containers for running the Jenkins pipeline.

  • Docker container has: git, android SDK, fastlane

Let’s write Pipeline as code

Create ‘Jenkinsfile’ in the root directory and paste the below code.

slack_channel = 'android-ci-cd'

pipeline
{ 
    agent{
        docker{
            image 'mindbowser/android-30-sdk:1.0'
            args '-u root:root'
        }
    }
    stages{
        stage('Init')
        {
            //check git commit message contains "skip ci" if found don't run the pipeline
             
            steps {
                script {

                    lastCommitInfo = sh(script: "git log -1", returnStdout: true).trim()
                    
                    commitContainsSkip = sh(script: "git log -1 | grep 'skip ci'", returnStatus: true)
                    
                    slackMessage = "*${env.JOB_NAME}* *${env.BRANCH_NAME}* received a new commit. \nHere is commmit info: ${lastCommitInfo}\n*Console Output*: <${BUILD_URL}/console | (Open)>"
                    
                    slack_send(slackMessage)

                    if(commitContainsSkip == 0) {
                        skippingText = " Skipping Build for *${env.BRANCH_NAME}* branch."
                        currentBuild.result = 'ABORTED'
                        slack_send(skippingText,"warning")
                        error('BUILD SKIPPED') 
                    }
                }
            }
        }
        stage('build')
        {
            // call fastlane lane for generate apk and uploading to testflight
                 steps{

                    sh "chmod +x gradlew"
                    
                    sh "chmod +x Gemfile"
                    
                    sh "fastlane build --env ${env.BRANCH_NAME}"    //eg. fastlane build --env development
                }
        }

    }
    post {
        always {
            // delete the workspace

            sh "chmod -R 777 ."
            
            deleteDir() 
        }
        success{
            
             slack_send("Jenkins job  for *${env.BRANCH_NAME}* completed successfully. ","#0066ff")
        }
        aborted{
            slack_send("Jenkins job  for *${env.BRANCH_NAME}* Skipped/Aborted.","warning")
        }
        failure {
            
          slack_send("*${env.BRANCH_NAME}* Something went wrong.Build failed. Check here: Console Output*: <${BUILD_URL}/console | (Open)>","danger")
        }
    }

}

def slack_send(slackMessage,messageColor="good")
{
    slackSend channel: slack_channel , color: messageColor, message: slackMessage
}

The above file contains two-stage:

In the first stage we check the commit message, if it contains ‘skip ci’ then Jenkins job is aborted.

The next stage is the build stage, which we call fastlane build lane which will generate and distribute the app according to the branch.

Create Jenkins job

Prerequisite:

BitBucket Branch Source Plugin:

Slack plugin:

Click on a new item

Enter pipeline name and select multibranch pipeline type

Add name, description and select your branch source

Select your SCM provider.

Add credentials and then select the repository name from the drop-down list

Now select your branch discovery strategy, In my case, I want to generate build only when commits to the following branches

  • Development
  • Staging
  • Master

For that, first, we will select all branches and then will filter them out.

Now add space-separated branch names

After that, Save this configuration.

It will scan the branches and select the branches which have jenkinsfile and we are filtered.

 

Note: To make sure the build gets triggered, add a webhook to the repository.

http://<jenkins-url>/bitbucket-scmsource-hook/notify

coma

Conclusion

In this blog, we learned How to implement CI/CD for android app development? With these strategies, you can speed up your development time and reduce feedback loop time.

Shubham

DevOps Engineer

Shubham is DevOps Engineer at Mindbowser Global Inc. Shubham has hands-on experience in implementing automation, CI/CD pipelines and DevOps processes. He loves to write blogs on developers’ technical issues in day-to-day work and guide them through his tech blogs.

The founder's survey report on "What Matters For Startup" is released - Get your copy and learn the trends of successful companies :)

Download Free eBook Now!

Get in touch for a detailed discussion.

Hear From Our 100+ Customers
coma

Mindbowser helped us build an awesome iOS app to bring balance to people's lives.

author
Addie Wootten
CEO, Smiling Mind
coma

We had very close go live timeline and MindBowser team got us live a month before.

author
Shaz Khan
CEO, BuyNow WorldWide
coma

They were a very responsive team! Extremely easy to communicate and work with!

author
Kristen M.
Founder & CEO, TotTech
coma

We’ve had very little-to-no hiccups at all—it’s been a really pleasurable experience.

author
Chacko Thomas
Co-Founder, TEAM8s
coma

Mindbowser is one of the reasons that our app is successful. These guys have been a great team.

author
Dave Dubier
Founder & CEO, MangoMirror