There are different secrets we use when developing an mobile app. For example before distributing our app we need to sign it with our private release key or when we want to access an API we need to authenticate ourselves with an API key. Those information should not be in the source code repository. They should only be accessible by certain people or the build server but not by every developer in the team.

This article shows how to handle secrets in flutter projects using the example of adding a Google Maps Api Key for Android and iOS.

General idea

The general idea is to use environment variables to keep your secrets out of the source code repository. Most CI Tools like Travis CI, Gitlab CI or Github Actions allow to configure secrets which are then available during the build as environment variables.

Example configuring secrets for a github project
Example configuring secrets for a github project

So, how do we use those environment variables, in our case MAPS_API_KEY in Android and iOS?

Android

First add following code to the top of ./android/app/build.gradle

def mapsProperties = new Properties()
def localMapsPropertiesFile = rootProject.file('local_maps.properties')
if (localMapsPropertiesFile.exists()) {
    project.logger.info('Load maps properties from local file')
    localMapsPropertiesFile.withReader('UTF-8') { reader ->
        mapsProperties.load(reader)
    }
} else {
    project.logger.info('Load maps properties from environment')
    try {
        mapsProperties['MAPS_API_KEY'] = System.getenv('MAPS_API_KEY')
    } catch(NullPointerException e) {
        project.logger.warn('Failed to load MAPS_API_KEY from environment.', e)
    }
}
def mapsApiKey = mapsProperties.getProperty('MAPS_API_KEY')
if(mapsApiKey == null){
    mapsApiKey = ""
    project.logger.error('Google Maps Api Key not configured. Set it in `local_maps.properties` or in the environment variable `MAPS_API_KEY`')
}

This code first tries to read local_maps.properties. This is used to supply a development MAPS_API_KEY on the developer machine. The properties file looks like this:

# Do not add to version control, this contains your local development configurations
MAPS_API_KEY=<YOUR-API-KEY>

If it can't find a properties file it loads the key from the environment variable. So of course you also could just set the environment variables on your local developer machine instead of using a properties file.

For Google Maps we need to add this key to the AndroidManifest. Therefore we add the previously loaded key to the defaultConfig in ./android/app/build.gradle:

android {
    ...
    defaultConfig {
        ...
        manifestPlaceholders = [MAPS_API_KEY: mapsApiKey]
    }
    ...
}

and we can use this key in ./android/app/src/main/AndroidManifest.xml like this:

<manifest ...>

    <application
        android:name="io.flutter.app.FlutterApplication"
        ...>
        <meta-data android:name="com.google.android.geo.API_KEY"
               android:value="${MAPS_API_KEY}"/>
        ...
    </application>
</manifest>

And that's it. We read the Google Maps Api key from the environment and used it in the AndroidManifest.xml

iOS

For iOS we add the environment variable by writing a script which reads the variable and generates swift code for us which we can use to access the api key and pass it to the Google Services.

1. Install Sourcery which is used for code generation.

  • Add pod 'Sourcery', '~> 1.0.0' to ./ios/Podfile right.
...
target 'Runner' do
use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
pod 'Sourcery', '~> 1.0.0'
end
  • Run pod install to install Sourcery.

2. Add scripts to generate swift code containing your credentials

  • Create folder `./ios/credentials
  • On your local development machine you can add a script which sets the environment variables for you. Do not add it to version control.
#!/bin/bash
# DO NOT CHECK INTO VERSION CONTROL
export MAPS_API_KEY=<YOUR-KEY>
  • Add a template for generating the Swift class ./credentials/Credentials.stencil
public struct Credentials {
	let mapsApiKey: String
}
public let credentials = Credentials(mapsApiKey: "{{ argument.mapsApiKey }}") 

This is the template used by Sourcery. It takes on argument mapsApiKey and replaces it in the code.

  • Add a script which does the code generation ./ios/credentials/generateCredentialsCode.sh:
#!/bin/bash

echo "Generate Credentials Code"
CREDENTIALS_DIR="$SRCROOT/credentials"

# Set credentials if local script for adding environment variables exist
if [ -f "$CREDENTIALS_DIR/add_credentials_to_env.sh" ]; then
  echo "Add credentials to environement"
  source "$CREDENTIALS_DIR/add_credentials_to_env.sh"
fi

$SRCROOT/Pods/Sourcery/bin/sourcery --templates "$CREDENTIALS_DIR/Credentials.stencil" --sources . --output "$SRCROOT/Runner" --args mapsApiKey=$MAPS_API_KEY

This script calls Sourcery to generate a code file Credentials.generated.swift which we later can use in our code to access the api key. Make sure to add Credentials.generated.swift to .gitignore otherwise our key will end up in the repo again.

3. Add scripts to XCode Project

Last but not least we need to call our script from our XCode project before every build. Therefore:

  • Open the iOS Module in XCode
  • Select Runner > Build Phases > Add a "Run Script"-Phase before the "Compile Sources"-Phase
  • Enter /bin/sh ./credentials/generateCredentialsCode.sh inside the "Run Script" phase. The /bin/sh at the beginning avoids problems with permissions.
    Call generateCredentialsCode.sh from
    Call generateCredentialsCode.sh from "Run Script" phase in XCode

So now Credentials.generated.swift will be generated during every build and we can use credentials.mapsApiKey to access our key and e.g. intialize the Google Maps Service in `./ios/Runner/AppDelegate.swift:

import UIKit
import Flutter
import GoogleMaps

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey(credentials.mapsApiKey)
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Conclusion

Now we can use the same mechanism to add other api keys or secret variables to our app. You can find the code in action in the Munichways App which is open source.

Thanks

The iOS part is an adaption of the process described by Robin Malhotras in his article on Medium. Check it out!