Disparate Matters

Release A Flutter-built Android App For Testing In Google Play In 32 Simple Steps

As of: 11/26/2023

These are the steps I took to release my aforementioned Flutter-built app to an internal test track in the Google Play Store. Where I thought it would be helpful for others, I've included notes identifying obstacles I had to get around in order to proceed. If you attempt to follow these steps and something has changed since I published this post, please do reach out (via the link at the bottom of the page) and let me know what you had to do differently, so I can update my instructions accordingly.

I used Android Studio for my Flutter development but you can use Visual Studio Code, which also has Flutter/Dart extensions.

OK, here goes!

Created a Google Play Account:

  1. Opened browser, navigated to: https://play.google.com/console/signup
    NOTE: If you are signed in to a Google account already, it will be assumed you want to use that for your Play Account. If you want to use/create a different account or if you're not signed in to a Google account, follow the relevant path Google will take you through.
  2. Chose type: An organization
    1. Entered Developer name
    2. Clicked Create a payments profile and entered D-U-N-S number for my organization
    3. Selected Organization Type: A company or business
    4. Selected Organization Size: 1-10
    5. Entered Organization Phone Number
    6. Entered Organization Website
    7. Entered a contact phone number and email address for Google Play users
    8. Entered a contact phone number and email address for Google
    9. Paid $25 (one-time) registration fee
      NOTE: Because I created an organization type account, I was then prompted to provide business verification documentation. I used the following, though there are various options for each type of document Google requires.
    10. Uploaded EIN notice from IRS
    11. Uploaded photos of front and back of my driver's license
    12. Provided mailing address

  3. NOTE: Though it said I might have to wait a few days for them to complete my business verification, it only took about 12 hours. :)

Added a Launcher Icon:

  1. Used https://imageresizer.com/ to resize the .png file, from vector set I purchased, to five different sizes, per Android standards (/android/app/src/main/res/):
    1. mipmap-hdpi: 72x72
    2. mipmap-mdpi: 48x48
    3. mipmap-xhdpi: 96x96
    4. mipmap-xxhdpi: 144x144
    5. mipmap-xxxhdpi: 192x192
  2. Copied each of the resized icons ^^^ into respective folder (e.g., "mipmap-hdpi", "mipmap-mdpi", etc.) and renamed each icon to "[MY APP NAME]_launch.png"
  3. In AndroidManifest.xml, updated:
    1. the application tag’s android:label attribute to "[MY APP NAME]"
    2. the application tag’s android:icon attribute to "@mipmap/[MY APP NAME]_launcher"
  4. In build.gradle, (per: https://developer.android.com/build/configure-app-module#change_the_package_name):
    1. Changed "defaultConfig { applicationId" to "com.[MY COMPANY NAME].[MY APP NAME WITH NO PUNCTUATION]" (to match what I used for iOS app, not that it REALLY matters, but I like consistency when I can have it. :) )
    2. Left "android { namespace" as "com.[MY COMPANY NAME].[MY APP NAME WITH AN UNDERSCORE BETWEEN WORDS]"
      NOTE: While it is tempting to change this to match the applicationId, because I created my Flutter project named "[MY APP NAME WITH AN UNDERSCORE BETWEEN WORDS]" the resultant Kotlin package name was created as that way also so I need to leave this as-is or things break without additional refactoring I didn't want to deal with.
  5. I confirmed that the default icon was successfully replaced with my custom one, by running the app and inspecting the app icon in the Launcher

NOTE: If your app uses Platform Views, you might want to enable Material Components by following the steps described in the Getting Started guide for Android (https://flutter-ko.dev/deployment/android#enabling-material-components

Signed the App:

NOTE: To publish on the Play Store, you need to sign your app with a digital certificate.

Android uses two signing keys: upload and app signing.

  1. Created an upload keystore:
    1. From project root, ran the following command:
      $ flutter doctor -v
    2. Decided on a directory on local computer to save keystore ([KEYSTORE_DIR])
    3. Chose a strong password to use for keystore creation and saved it somewhere secure
    4. In the resultant output, under "Android toolchain...", found and saved path value following "Java binary at:"
    5. Ran the following command, replacing [JAVA_BINARY_AT] with saved path value MINUS "/java" and [KEYSTORE_DIR] with literal value chosen above:
      $ [JAVA_BINARY_AT]/keytool -genkey -v -keystore [KEYSTORE_DIR]/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
    6. Entered Password
    7. Entered First and last name
    8. Entered Organizational Unit: Mobile
    9. Entered Organization: [my company name]
    10. Entered City: [my company's city name]
    11. Entered State: [my company's state name, spelled out]
    12. Entered Country Code: US
    13. Entered "yes" after reviewing my entered data
    14. Confirmed I could see keystore.jks file had been created in specified dir
  2. Referenced the keystore from the app:
    Created a file named [project]/android/key.properties that contains a reference to the keystore, replacing values in [] with literal values:
    storePassword=[password-from-previous-step]
    keyPassword=[password-from-previous-step]
    keyAlias=upload
    storeFile=[KEYSTORE_DIR]/upload-keystore.jks
    
  3. Configured signing in Gradle:
    1. Configured gradle to use the upload key when building app in release mode by editing the [project]/android/app/build.gradle file:
      1. Added the keystore information from the properties file before the android block:
        def keystoreProperties = new Properties()
        def keystorePropertiesFile = rootProject.file('key.properties')
        if (keystorePropertiesFile.exists()) {
            keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
        }
        
        android {
            ...
        }
        
      2. Found the buildTypes block:
        buildTypes {
            release {
                // TODO: Add your own signing config for the release build.
                // Signing with the debug keys for now,
                // so `flutter run --release` works.
                signingConfig signingConfigs.debug
            }
        }
        
      3. And replaced it with the following signing configuration info:
        signingConfigs {
            release {
                keyAlias keystoreProperties['keyAlias']
                keyPassword keystoreProperties['keyPassword']
                storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
                storePassword keystoreProperties['storePassword']
            }
        }
        buildTypes {
            release {
                signingConfig signingConfigs.release
            }
        }
        
  4. Cleared out old build settings in order to pick up Gradle (and other) updates, by running the following command:
    $ flutter clean

NOTE: If your app is large or uses large plugins, you might need to enable multidex support (https://flutter-ko.dev/deployment/android#enabling-multidex-support)

Reviewed the gradle.build configuration:

NOTE: Depending on how you've built your app, you may need/want to change some/all of these settings:

  1. Confirmed the following settings defined under the defaultConfig block:
    1. applicationId: (already done in "Add a Launcher Icon" section above)
    2. minSdkVersion: (left as "flutter.minSdkVersion")
    3. targetSdkVersion: (left as "flutter.targetSdkVersion")
    4. versionCode: (left as "flutter.versionName" [from local.properties])
    5. versionName: (left as "flutter.versionCode" [from local.properties])
    6. buildToolsVersion: (left out as I didn't need this)
  2. Confirmed the following settings defined under the android block:
    compileSdkVersion: (left as "flutter.compileSdkVersion")

NOTE: If you change any of these you probably want to re-run:
$ flutter clean

Built the app for release:

  1. Built an app bundle from within Flutter project directory:
    $ cd [project]
    $ flutter build appbundle --obfuscate --split-debug-info=v1.1.0+1
  2. Confirmed the release bundle (app-release.aab) was created in the project's "build/app/outputs/bundle/release" directory

Created And Set Up App in Play Console:

  1. Created and set up app (per: https://support.google.com/googleplay/android-developer/answer/9859152):
    1. Opened Play Console
    2. Selected All apps > Create app
    3. Added the name of app as I want it to appear on Google Play
    4. Selected a default language: English-US (en-US)
    5. Specified whether my application is an app or a game: App
    6. Specified whether my application is free or paid: Free
    7. In the "Declarations" section:
      1. Acknowledged the “Developer Program Policies” declaration
      2. Accepted the Play App Signing Terms of Service
      3. Acknowledged the “US export laws” declaration
    8. Pressed Create app

    NOTE: Under "Start testing now" a message displayed: "Release your app early for internal testing without review"

Set up internal testing track:

  1. Still in Play Console, in left pane, under Release >> Testing, selected Internal Testing
  2. Under Testers, clicked "Create email list":
    1. Entered a List name
    2. Added testers' email addresses, pressing Enter to submit each
    3. Pressed Save changes when all testers added
  3. Entered a Feedback email address
  4. Pressed Save

Created a new release:

  1. Remaining in Play Console, under App bundles, clicked "Choose signing key":
    Pressed Use Google-generated key
    NOTE: Under App integrity, it now says "Releases signed by Google Play"
  2. Under App bundles, click Upload, navigated to location on local harddrive where release bundle (app-release.aab) was created (e.g., "the project's "build/app/outputs/bundle/release" directory)
  3. Entered Release details:
    1. Left Release name as "1 (1.0.0)" (defaulted by Google Play)
    2. Entered Release notes text
    3. Press Next
      NOTE: Received warning: "This App Bundle contains native code, and you've not uploaded debug symbols. We recommend you upload a symbol file to make your crashes and ANRs easier to analyze and debug."
      Choose to ignore this for now...
    4. Pressed Save
  4. Under Releases, clicked "View release details" and confirmed "Release summary status" details looked reasonable
  5. Under Testers >> "How testers can join your test" >> "Join on the web", confirmed that "Testers can join your test on the web" link has updated from a placeholder to a Play Store URL specific to an internal test (e.g., "https://play.google.com/apps/internaltest/...")
  6. Copied this URL for subsequent usage ([APP_STORE_TEST_URL])

Installed on Android Phone:

NOTE: I am already (effectively "permanently") signed into my Google account on my Android phone; if you're somehow not, you might be prompted to sign in to your Google account during the following sequence.

  1. On phone browser, navigated to [APP_STORE_TEST_URL]
  2. Pressed ACCEPT INVITE
  3. Clicked "Download it on Google Play"
  4. Clicked "Open in other app"
  5. Re-directed to Google Play >> "[APP_NAME] (unreviewed)" screen
  6. Pressed Install
  7. Pressed Open App

I then navigated to my phone's home screen and could see my app, complete with my custom app icon. I opened it and it ran, as expected (based on experience represented by emulator back in Android Studio).