ALL ARTICLES
SHARE

React Native Android Deploys using Fastlane and CircleCI

author-avatar
Development
20 min read

In this article, we will guide you toward a solid automatic Android deployment solution leveraging React Native, Fastlane, and CircleCI. React Native offers an intuitive and responsive user interface and allows us to create apps for multiple platforms from the same code base. Fastlane provides developers with a full set of tools and actions to speed up and automate the mobile release process. Finally, CircleCI completes the solution by seamlessly adding continuous integration into the solution, making the build process simple, automatic, and repeatable. 

Before Setting Up

There are a number of prerequisite steps that must be completed before we can officially begin setting up our solution. Before going any further we need:

Fastlane Configuration

Go to your Android folder and initiate Fastlane

Getting started with Fastlane is pretty simple. In your android folder, use bash to initiate Fastlane with the following line of code.

cd android Fastlane init
cd android Fastlane init

Next, follow these steps: 

1. The package-name can be found in the android/app/build.gradle file as applicationId:

Note: If you are using a different applicationId, depending on the flavor of Android being built, you can just set the default one at this point. We’ll cover how to submit multiple flavors later in this article.

2. For the step asking the ‘Path to the JSON secret file’, paste the following: ./Fastlane/playstore-credentials.json.

3. When asked to ‘Download existing metadata and setup metadata management?’, type ‘N’, unless you want to download the metadata. We will not cover that topic in this post, but if downloading the metadata, be sure to remember to add it to the .gitignore file. See this example for more information. Once finished, add your `play-store-credentials.json` to the Fastlane folder that was just created. Then, add this file to the .gitignore file, as we don’t want to have this in our repo.

android/fastlane/play-store-credentials.json

 

Lane Setup

Next, we are going to set up two lanes for automated deployment, one for staging, and another for production. A laneis a group of instructions to be executed in a particular order, similar to a function in JavaScript. Feel free to add as many lanes as desired for your deployment. The process is largely the same for each lane.

Go to your Fastfile (fastlane/Fastfile) and remove all the content inside platform :android do, the remaining file should look like this:

default_platform(:android)

platform :android do

end
default_platform(:android)

platform :android do

end

Now, let’s create some lanes! All of them should be created within platform :android doand the end statement.

Staging Lane

 First, we’re going to create a lane called ‘Staging’. It is always best practice to provide a description before the lane definition. 

desc "Submit a new Staging Build to Google Play Internal Track"
  lane :staging do
  end 

desc "Submit a new Staging Build to Google Play Internal Track"
  lane :staging do
  end 

Inside this staging, add the ‘gradle’ instructions to create the build.

Gradlerequires, which can bundleor assemble. We are going to choose the bundle option. The flavor for our example will be, and the build type will be Debugor Release. Since we are planning to submit to the store, it makes sense to create a Release.

gradle(
  task: "bundle",
  flavor: "Staging",
  build_type: "Release"
)

gradle(
  task: "bundle",
  flavor: "Staging",
  build_type: "Release"
)

In the next line, add the `upload_to_play_store` command to submit the build to Google Play Store.

Upload_to_play_store requires a track, which can be any of the tracks available on the Google Play Store. For the sake of simplicity, we are using the internaltrack to submit staging builds. Package_nameis optional if you are not using flavors.

upload_to_play_store(
   track: 'internal',
   package_name: 'com.flatirons.androidStaging'
) 

upload_to_play_store(
   track: 'internal',
   package_name: 'com.flatirons.androidStaging'
) 

If you want to run this command on your machine, it is recommended to do a clean before generating a new build, so add this at the beginning of the staging lane.

gradle(task: "clean bundleStagingRelease") 

gradle(task: "clean bundleStagingRelease") 

When done, your Fastfile should look something like this: 

default_platform(:android)

platform :android do
  desc "Submit a new Staging Build to Google Play Internal Track"
  lane :staging do
    gradle(task: "clean bundleStagingRelease")
    gradle(
      task: "bundle",
      flavor: "Staging",
      build_type: "Release"
    )
    upload_to_play_store(
      track: 'internal',
      package_name: 'com.flatirons.androidStaging'
    )
  end
end 


default_platform(:android)

platform :android do
  desc "Submit a new Staging Build to Google Play Internal Track"
  lane :staging do
    gradle(task: "clean bundleStagingRelease")
    gradle(
      task: "bundle",
      flavor: "Staging",
      build_type: "Release"
    )
    upload_to_play_store(
      track: 'internal',
      package_name: 'com.flatirons.androidStaging'
    )
  end
end 


Production Lane

Creating a lane is largely the same process, regardless of function or purpose. To create a ‘production’ lane, only a few changes need to be made. All you have to do is change the lane name to :production and thepackage_name if you are using flavors. 

CircleCI Config

To introduce continuous integration into our solution, we are going to leverage CircleCI, and more specifically, CircleCI Orbs to improve our deployment solution. An orb is a reusable snippet of code that helps to automate repeated processes, accelerate project setup, and integrate 3rd party tools. CircleCI provides an open registry of published Orbs, making it easy to find and leverage an Orb that suits your needs.  

For our deployment, we will be leveraging the react-native-community’s orb, allowing us to outsource the environment configuration and maintenance. This orb also includes tools for testing, but we will not be using those for this deployment. If interested in end-to-end testing, see this post for more info.

First, create a CircleCI Config.yaml file. 

touch .circleci/config.yml 

touch .circleci/config.yml 

Add the version, orbs dependencies, then create 3 keywords in that file. Indentation is very important in this file.

yml
version: 2.1 

yml
version: 2.1 

Make sure you use the latest version of the Orb!

orbs:
  rn: react-native-community/react-native@7.1.1

commands:

jobs:

workflows:
orbs:
  rn: react-native-community/react-native@7.1.1

commands:

jobs:

workflows:

To verify you have a valid config.yml file, you can run the circleci orb validate .circleci/config.ymlcommand in your root folder.

In the orbs section, we are defining a dependency named ‘rn’, (this alias is used to reference all the predefined tasks in this orb but can be changed to anything you want).

The commandssection is a place to define reusable instructions, typically used by the jobs.

Jobsare detailed sets of instructions to be run in workflows.

Workflowis a place where we can set rules and the order in which jobs are run, for example, ensuring a job is run on a specific branch, or creating dependencies between jobs (such as not running fastlane deployments if tests are failing).

With the above in mind, let’s add some useful commands to our commandssection.

yml
commands:
  install_packages:
    steps:
      - restore_cache:
          key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
      - restore_cache:
          key: node-v1-{{ checksum "package.json" }}-{{ arch }}
      - run: yarn install
      # Uncomment the following line if you need to patch your packages
      # - run: yarn patch-package
      - save_cache:
          key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
          paths:
            - ~/.cache/yarn
      - save_cache:
          key: node-v1-{{ checksum "package.json" }}-{{ arch }}
          paths:
            - node_modules

yml
commands:
  install_packages:
    steps:
      - restore_cache:
          key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
      - restore_cache:
          key: node-v1-{{ checksum "package.json" }}-{{ arch }}
      - run: yarn install
      # Uncomment the following line if you need to patch your packages
      # - run: yarn patch-package
      - save_cache:
          key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
          paths:
            - ~/.cache/yarn
      - save_cache:
          key: node-v1-{{ checksum "package.json" }}-{{ arch }}
          paths:
            - node_modules

The above install_packages command is used to do a yarn install, and any other operations related to the node_modules.

create_android_key_files:
    steps:
      - checkout
      - run:
          name: Decrypting Key Store
          command: |
            echo $ANDROID_ENCRYPTED_KEY_STORE | base64 -d > ./android/app/release.keystore
      - run:
          name: Decrypting Google Play Upload Key
          command: |
            echo $GOOGLE_PLAY_SERVICES_UPLOAD_KEY | base64 -d > ./android/fastlane/play-store-credentials.json
      - persist_to_workspace:
          paths: .
          root: . 

create_android_key_files:
    steps:
      - checkout
      - run:
          name: Decrypting Key Store
          command: |
            echo $ANDROID_ENCRYPTED_KEY_STORE | base64 -d > ./android/app/release.keystore
      - run:
          name: Decrypting Google Play Upload Key
          command: |
            echo $GOOGLE_PLAY_SERVICES_UPLOAD_KEY | base64 -d > ./android/fastlane/play-store-credentials.json
      - persist_to_workspace:
          paths: .
          root: . 

Create_android_key_files will be used to create the release.keystore and the play-store-credentials.json mentioned earlier in the article.

 
create_env_file:
    parameters:
      api_url:
        type: string
    steps:
      - checkout
      - run:
          name: Create Env File
          command: |
            echo 'Creating .env file!'
            touch .env
            echo "API_URL=<>">>.env
      - persist_to_workspace:
          paths: .
          root: . 

 
create_env_file:
    parameters:
      api_url:
        type: string
    steps:
      - checkout
      - run:
          name: Create Env File
          command: |
            echo 'Creating .env file!'
            touch .env
            echo "API_URL=<>">>.env
      - persist_to_workspace:
          paths: .
          root: . 

The create_env_file will be used to generate the .env file, you can ignore this if you are not using env config.

 
install_fastlane:
    steps:
      - checkout
      - run:
          command: gem install bundler
          name: Install bundler
      - run:
          command: gem install fastlane
          name: Install Fastlane
      - persist_to_workspace:
          paths: .
          root: . 
 
install_fastlane:
    steps:
      - checkout
      - run:
          command: gem install bundler
          name: Install bundler
      - run:
          command: gem install fastlane
          name: Install Fastlane
      - persist_to_workspace:
          paths: .
          root: . 

Finally, we have the install_fastlane command, which provides instructions to install the latest fastlane version.

Now, we are ready to use these commands to create the jobs:

 
yml
jobs:
  # Env configurations
  checkout_code:
    working_directory: ~/repo
    environment:
      TZ: "America/New_York"
    docker:
      - image: circleci/node:latest
    steps:
      - checkout
      - persist_to_workspace:
          paths: .
          root: .
 
yml
jobs:
  # Env configurations
  checkout_code:
    working_directory: ~/repo
    environment:
      TZ: "America/New_York"
    docker:
      - image: circleci/node:latest
    steps:
      - checkout
      - persist_to_workspace:
          paths: .
          root: .


Checkout_code uses CircleCI’s checkout command

  
create_staging_env_file:
    working_directory: ~/repo
    docker:
      - image: circleci/node:latest
    steps:
      - create_env_file:
          api_url: $API_URL_STAGING 

  
create_staging_env_file:
    working_directory: ~/repo
    docker:
      - image: circleci/node:latest
    steps:
      - create_env_file:
          api_url: $API_URL_STAGING 

Create_staging_env_file uses the code>create_env_file command to generate the .env file with staging-specific environment variables.

 
create_production_env_file:
    working_directory: ~/repo
    docker:
      - image: circleci/node:latest
    steps:
      - create_env_file:
          api_url: $API_URL_PRODUCTION 
 
create_production_env_file:
    working_directory: ~/repo
    docker:
      - image: circleci/node:latest
    steps:
      - create_env_file:
          api_url: $API_URL_PRODUCTION 

Create_production_env_file uses the create_env_file command to generate the .env file with production-specific environment variables.

 
# Testing
  code_testing:
    working_directory: ~/repo
    executor:
      name: rn/linux_js
      node_version: "16.5"
    steps:
      - attach_workspace:
          at: .
      - install_packages
      - run:
          command: yarn test
          name: Run Tests
      # Add more commands if you need to test lint or typescript 

 
# Testing
  code_testing:
    working_directory: ~/repo
    executor:
      name: rn/linux_js
      node_version: "16.5"
    steps:
      - attach_workspace:
          at: .
      - install_packages
      - run:
          command: yarn test
          name: Run Tests
      # Add more commands if you need to test lint or typescript 

As you might guess from the name, code_testing runs tests on the app.

# Deployment
  fastlane_android_release:
    working_directory: ~/repo
    executor:
      name: rn/linux_android
      gradle_options: "-Xmx4608m -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
      java_options: "-Xmx4608m -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
    resource_class: large
    parameters:
      lane:
        type: string
      label:
        type: string
    steps:
      - attach_workspace:
          at: .
      - install_packages
      - install_fastlane
      - create_android_key_files
      - run:
          command: cd android && fastlane << parameters.lane >>
          name: Upload << parameters.label >> build to Google Play

# Deployment
  fastlane_android_release:
    working_directory: ~/repo
    executor:
      name: rn/linux_android
      gradle_options: "-Xmx4608m -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
      java_options: "-Xmx4608m -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
    resource_class: large
    parameters:
      lane:
        type: string
      label:
        type: string
    steps:
      - attach_workspace:
          at: .
      - install_packages
      - install_fastlane
      - create_android_key_files
      - run:
          command: cd android && fastlane << parameters.lane >>
          name: Upload << parameters.label >> build to Google Play

Fastlane_android_release installs fastlane, and runs a specific lane. Be aware that resource_class: large is being used in this example. If your build can use a less demanding resource class, go for it. Learn more about resource_classes here.  The gradle_options and java_options defined in this job are there to allow the build creation to use more ram and avoid errors when generating the build.

Once these jobs are configured, all we need to do to complete our config.yml file is to define the workflow.

yml
workflows:
  flatirons_example:
    jobs:
      - checkout_code
      - code_testing:
          name: "Run tests"
          requires:
            - checkout_code

      - create_staging_env_file:
          name: "Create Staging .env"
          filters:
            branches:
              only:
                - staging
          requires:
            - code_testing

      - create_production_env_file:
          name: "Create production .env"
          filters:
            branches:
              only:
                - main
          requires:
            - code_testing

      ###############
      ### Staging ###
      ###############
      - fastlane_android_release:
          name: "Upload android staging build"
          lane: staging
          label: Staging
          filters:
            branches:
              only:
                - staging
          requires:
            - create_staging_env_file

      ################
      ## Production ##
      ################
      - fastlane_android_release:
          name: "Upload android production build"
          lane: production
          label: Production
          filters:
            branches:
              only:
                - main
          requires:
            - create_production_env_file 

yml
workflows:
  flatirons_example:
    jobs:
      - checkout_code
      - code_testing:
          name: "Run tests"
          requires:
            - checkout_code

      - create_staging_env_file:
          name: "Create Staging .env"
          filters:
            branches:
              only:
                - staging
          requires:
            - code_testing

      - create_production_env_file:
          name: "Create production .env"
          filters:
            branches:
              only:
                - main
          requires:
            - code_testing

      ###############
      ### Staging ###
      ###############
      - fastlane_android_release:
          name: "Upload android staging build"
          lane: staging
          label: Staging
          filters:
            branches:
              only:
                - staging
          requires:
            - create_staging_env_file

      ################
      ## Production ##
      ################
      - fastlane_android_release:
          name: "Upload android production build"
          lane: production
          label: Production
          filters:
            branches:
              only:
                - main
          requires:
            - create_production_env_file 

In this example, we’ve created a flatirons_example workflow, where code_testing needs to be successful in order to create the .env files that will later be used by the fastlane command when creating the build.

Using the filtersproperty, you can assign jobs to run on only specific branches.

The requiresparameter allows us to avoid running jobs if another one fails.

Additionally, with the namekey, more legible names can be displayed in the CircleCI dashboard.

CircleCI Environment Variables

As the last stage of our deployment, we will be configuring CircleCI, along with all the necessary environment variables. 

First, let’s go ahead and configure CircleCI by following these instructions.

As we have already created our .circleci/config.yml in the previous steps, we will now select the ‘Fastest: Use the .circleci/config.yml in my repo’ option.

In this example, we have two environment variables that will be used in the app (API_URL_STAGING, and API_URL_PRODUCTION), and others used by the config.yml (ANDROID_ENCRYPTED_KEY_STORE, and GOOGLE_PLAY_SERVICES_UPLOAD_KEY).

API_URL_STAGING and API_URL_PRODUCTION act as the URL for your API depending on the environment. To set these environment variables in CircleCI follow these instructions.

Before we do the same with ANDROID_ENCRYPTED_KEY_STORE and GOOGLE_PLAY_SERVICES_UPLOAD_KEY, we need to obtain the values for them.

To obtain these values, we are going to do a simple base64 encryption to get the file content as a string. Let’s start with the android release.keystore. Run the following command: 

cat ./android/path/to/release.keystore | base64 > encrypted.txt
cat ./android/path/to/release.keystore | base64 > encrypted.txt

Copy the content of this file, then add it as the ANDROID_ENCRYPTED_KEY_STOREenvironment variable in CircleCI, and delete the file.

Now, do the same with your play-store-credentials.json file.

cat ./android/Fastlane/play-store-credentials.json | base64 > encrypted.txt
cat ./android/Fastlane/play-store-credentials.json | base64 > encrypted.txt

Copy the content of this file, add it as the GOOGLE_PLAY_SERVICES_UPLOAD_KEY environment variable in CircleCI, and delete the file.

If you want to verify that the encrypted string is the same as within the file, you can decrypt the string, and check that files are the same by using this command:

echo "encrypted. Text Content" | base64 -d > output.txt
cmp --silent ./android/path/to/file output.txt || echo "Files do not match"
echo "encrypted. Text Content" | base64 -d > output.txt
cmp --silent ./android/path/to/file output.txt || echo "Files do not match"

If “Files do not match” is printed within the console, it means the string content was not correctly input. If nothing is printed, then the string content has been validated.

At this point, you should be able to do an automatic release when merging changes into the staging branch. But there is a major issue, all builds are being submitted with the same versionCode, this can generate conflict and in the majority of cases, the build will not be submitted.

To prevent this issue, in the android/app/build.gradle above the android section, define this function:

static def getBuildVersion() {
    def date = new Date()
    def formattedDate = (int)(date.getTime() / 1000);
    println("VersionCode: " + formattedDate)
    return formattedDate
} 
static def getBuildVersion() {
    def date = new Date()
    def formattedDate = (int)(date.getTime() / 1000);
    println("VersionCode: " + formattedDate)
    return formattedDate
} 

Set your versionCode to use this function:

versionCode getBuildVersion() as int
versionCode getBuildVersion() as int

And our deployment solution is good to go!

As a bonus, let’s show you how to have this environment set to send a slack message when the build is successfully created. By adding slackafter upload_to_play_store, and running the following command, a notification will be pushed to Slack upon completion of any build: 

slack(
   message: "[ANDROID][Flatirons Example]: New staging build available on Google Play Internal Track [STAGING]",
   slack_url: "https://api.slack.com/messaging/webhooks",
   success: true,
   payload: {}
)


slack(
   message: "[ANDROID][Flatirons Example]: New staging build available on Google Play Internal Track [STAGING]",
   slack_url: "https://api.slack.com/messaging/webhooks",
   success: true,
   payload: {}
)


And that’s it, you’ve just configured Fastlane and CircleCI, and can now sit down, relax, drink some coffee, read a book, or whatever floats your boat, until notified by Slack that your build has been completed!

Here you can check a complete guide to React Native and for working with React Native developers.

React Native Development Experts

Elevate your mobile apps with Flatirons' React Native expertise for cross-platform solutions.

Learn more

React Native Development Experts

Elevate your mobile apps with Flatirons' React Native expertise for cross-platform solutions.

Learn more
author-avatar
More ideas.
Development

tRPC vs GraphQL: Which API Is Best for You?

Flatirons

May 15, 2024
Development

Choosing the Best Flutter IDE for App Development

Flatirons

May 14, 2024
Development

PWA vs. Native Apps: A Comparison

Flatirons

May 13, 2024
Development

The Key Roles in a Software Development Team

Flatirons

May 11, 2024
Development

How to Write Clean and Maintainable Code

Flatirons

May 10, 2024
Development

How to Hire an Offshore Developer

Flatirons

May 09, 2024
Development

tRPC vs GraphQL: Which API Is Best for You?

Flatirons

May 15, 2024
Development

Choosing the Best Flutter IDE for App Development

Flatirons

May 14, 2024
Development

PWA vs. Native Apps: A Comparison

Flatirons

May 13, 2024
Development

The Key Roles in a Software Development Team

Flatirons

May 11, 2024
Development

How to Write Clean and Maintainable Code

Flatirons

May 10, 2024
Development

How to Hire an Offshore Developer

Flatirons

May 09, 2024