Skip to main content
mobile8 de fevereiro de 202612 min de leitura

Automatizando Deploys Android para a Google Play Store

Configurando builds e deploys automatizados para Android — desde configuração do Gradle até publicação na Play Store via CI/CD.

androidgradlecicd
Automatizando Deploys Android para a Google Play Store

Publicar um app Android na Play Store parece que deveria ser simples — compilar um APK, fazer upload, pronto. Na prática, o processo envolve configurações de assinatura, variantes de build, formatos de bundle, tracks da Play Store, rollouts escalonados e configuração Gradle suficiente para fazer qualquer um questionar suas escolhas de carreira. Depois de automatizar deploys Android em vários projetos, incluindo apps Flutter que precisam de pipelines tanto para iOS quanto para Android, eu tenho uma configuração que lida com tudo, do commit ao rollout em produção, sem intervenção manual.

O Sistema de Build Android

Antes de automatizar qualquer coisa, você precisa de um entendimento sólido de como os builds Android funcionam. Diferente do iOS, onde o Xcode lida com a maior parte da complexidade por trás de uma GUI, o sistema de build do Android é inteiramente baseado em Gradle e configurado através de código. Isso é na verdade uma vantagem para automação — tudo é explícito e versionado.

Configuração Gradle para Builds de Release

Seu android/app/build.gradle (ou build.gradle.kts se você migrou para Kotlin DSL) é o arquivo central de configuração. Para builds de release, você precisa configurar assinatura, minificação e otimização.

android {
    compileSdkVersion 35

    defaultConfig {
        applicationId "com.yourcompany.yourapp"
        minSdkVersion 24
        targetSdkVersion 35
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    signingConfigs {
        release {
            keyAlias System.getenv("KEY_ALIAS") ?: properties["keyAlias"]
            keyPassword System.getenv("KEY_PASSWORD") ?: properties["keyPassword"]
            storeFile file(System.getenv("KEYSTORE_PATH") ?: properties["keystorePath"] ?: "keystore.jks")
            storePassword System.getenv("STORE_PASSWORD") ?: properties["storePassword"]
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

A configuração de assinatura lê de variáveis de ambiente primeiro, depois faz fallback para um arquivo de propriedades local. Esse padrão permite que desenvolvedores assinem builds localmente usando um arquivo key.properties enquanto o CI usa variáveis de ambiente — o mesmo arquivo Gradle funciona em ambos os contextos sem modificação.

O Arquivo de Propriedades Local

Para desenvolvimento local, crie um arquivo key.properties no seu diretório android/ (e adicione-o ao .gitignore imediatamente):

storePassword=your_store_password
keyPassword=your_key_password
keyAlias=your_key_alias
keystorePath=../keys/release-keystore.jks

Depois referencie-o no seu build.gradle:

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

Configuração de Assinatura

A assinatura de apps Android é mais simples que o code signing do iOS, mas carrega um risco permanente: se você perder sua chave de upload, perde a capacidade de atualizar seu app. O Google introduziu o Play App Signing para mitigar isso, e eu recomendo fortemente usá-lo.

Play App Signing

Com o Play App Signing habilitado, o Google mantém a chave de distribuição real. Você assina seus uploads com uma chave de upload, e o Google reassina com a chave de distribuição antes de entregar aos usuários. Os benefícios:

  • Recuperação de chave perdida: Se você perder sua chave de upload, o Google pode resetá-la. Sem Play App Signing, perder sua chave significa criar uma nova listagem de app.
  • APKs menores: O Google pode otimizar o app para cada configuração de dispositivo usando a chave de distribuição.
  • Rotação de chave: Você pode rotacionar sua chave de upload sem afetar usuários instalados.

Habilite no Play Console em Configurações > Assinatura de app. Para novos apps, é habilitado por padrão.

Gerando um Keystore

Se você precisa criar um novo keystore:

keytool -genkey -v \
  -keystore release-keystore.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias your_key_alias

Armazene este arquivo keystore com segurança. Para CI, eu codifico em base64 e armazeno como um secret do GitHub Actions:

base64 -i release-keystore.jks | pbcopy

Depois decodifico no workflow do CI antes de compilar:

- name: Decode keystore
  run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

AAB vs APK

O Google agora exige Android App Bundles (AAB) para todos os novos apps na Play Store. A diferença importa para seu pipeline de build.

APK (Android Package) é um arquivo único contendo tudo para todas as configurações de dispositivo. É maior, mas universal — qualquer dispositivo Android pode instalar qualquer APK. APKs ainda são úteis para distribuição interna, testes em dispositivos físicos e distribuição fora da Play Store.

AAB (Android App Bundle) contém todo o código e recursos mas permite que o Google Play gere APKs otimizados para cada dispositivo. Um usuário com um telefone Pixel baixa apenas os recursos que precisa, não os assets para cada densidade de tela e arquitetura de CPU. Bundles são tipicamente 15-30% menores que APKs universais.

Para seu pipeline de CI, compile ambos:

# For Play Store
flutter build appbundle --release

# For internal testing / direct installation
flutter build apk --release --split-per-abi

A flag --split-per-abi gera APKs separados para cada arquitetura de CPU (arm64-v8a, armeabi-v7a, x86_64), o que reduz o tamanho do arquivo para distribuição direta.

Fastlane Supply para Play Store

A ação supply do Fastlane lida com uploads e gerenciamento de metadados da Play Store, espelhando o que o deliver faz para iOS.

Configurando o Supply

Primeiro, você precisa de uma conta de serviço do Google Play com as permissões corretas.

  1. Vá ao Google Cloud Console e crie uma conta de serviço.
  2. Baixe o arquivo de chave JSON.
  3. No Play Console, vá em Configurações > Acesso à API e vincule a conta de serviço.
  4. Conceda permissões de "Release manager" para seu app.
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_KEY"] || "path/to/service-account.json")
package_name("com.yourcompany.yourapp")

Upload Básico

default_platform(:android)

platform :android do
  desc "Deploy to Play Store Internal Testing"
  lane :internal do
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "./"
    )

    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

Para projetos Flutter, eu prefiro executar o comando de build do Flutter diretamente em vez de usar a task Gradle pelo Fastlane, porque o processo de build do Flutter inclui passos de compilação Dart que o Gradle sozinho não lida corretamente:

lane :internal do
  Dir.chdir("..") do
    sh("flutter build appbundle --release")
  end

  supply(
    track: "internal",
    aab: "../build/app/outputs/bundle/release/app-release.aab",
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Tracks da Play Store

A Play Store tem quatro tracks, e entender quando usar cada um é importante para sua estratégia de release.

Teste Interno

  • Até 100 testadores.
  • Sem revisão necessária — builds ficam disponíveis quase instantaneamente.
  • Melhor para: testes do time de desenvolvimento, ciclos de QA, verificação de builds antes de promover.

Teste Fechado (Alpha)

  • Testadores ilimitados, mas você gerencia a lista.
  • Requer uma breve revisão do Google (geralmente algumas horas).
  • Melhor para: programas beta com usuários convidados, previews para clientes.

Teste Aberto (Beta)

  • Qualquer pessoa pode participar pela listagem da Play Store.
  • Requer revisão.
  • Melhor para: programas beta públicos, coleta de feedback antes de um lançamento amplo.

Produção

  • Disponível para todos os usuários.
  • Revisão completa necessária.
  • Suporta rollouts escalonados.

Promovendo Entre Tracks

O Fastlane torna a promoção entre tracks direta:

lane :promote_to_beta do
  supply(
    track: "internal",
    track_promote_to: "beta",
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :promote_to_production do
  supply(
    track: "beta",
    track_promote_to: "production",
    rollout: "0.1",  # 10% staged rollout
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Rollouts Escalonados

Rollouts escalonados são uma das maiores vantagens do Android sobre o iOS. Você pode lançar para uma porcentagem de usuários e aumentar gradualmente, monitorando taxas de crash e feedback dos usuários em cada estágio.

lane :staged_rollout do |options|
  percentage = options[:percentage] || "0.1"

  supply(
    track: "production",
    rollout: percentage,
    aab: "../build/app/outputs/bundle/release/app-release.aab"
  )
end

lane :increase_rollout do |options|
  percentage = options[:percentage] || "0.5"

  supply(
    track: "production",
    rollout: percentage,
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :complete_rollout do
  supply(
    track: "production",
    rollout: "1.0",
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Um cronograma típico de rollout:

  1. Dia 1: 10% — observe taxas de crash no Firebase Crashlytics.
  2. Dia 2-3: 25% — verifique feedback dos usuários e tickets de suporte.
  3. Dia 4-5: 50% — confirme que não há regressões de performance em escala.
  4. Dia 6-7: 100% — release completo.

Se algo der errado em qualquer estágio, você pode pausar o rollout e enviar uma correção sem afetar todos os usuários.

Workflow do GitHub Actions

Aqui está um workflow completo de CI/CD para Android:

name: Deploy to Play Store

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      track:
        description: 'Play Store track'
        required: true
        default: 'internal'
        type: choice
        options:
          - internal
          - alpha
          - beta
          - production

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'gradle'

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.27.x'
          channel: stable
          cache: true

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
          working-directory: android

      - name: Flutter dependencies
        run: flutter pub get

      - name: Decode keystore
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

      - name: Create service account file
        run: echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}' > android/play-store-key.json

      - name: Build and deploy
        working-directory: android
        env:
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
          KEYSTORE_PATH: app/keystore.jks
          PLAY_STORE_JSON_KEY: play-store-key.json
        run: bundle exec fastlane ${{ github.event.inputs.track || 'internal' }}

      - name: Cleanup secrets
        if: always()
        run: |
          rm -f android/app/keystore.jks
          rm -f android/play-store-key.json

Algumas coisas a notar sobre este workflow:

Runners Ubuntu funcionam bem para Android. Diferente do iOS, que requer macOS, builds Android rodam no Linux. Runners Ubuntu são mais baratos e iniciam mais rápido no GitHub Actions.

Java 17 é necessário para versões atuais do Android Gradle Plugin. Se seu projeto usa uma versão mais antiga do AGP, você pode precisar do Java 11, mas isso é cada vez mais raro.

O passo de cleanup remove secrets decodificados mesmo se o build falhar. Esta é uma medida de defesa em profundidade — runners do GitHub Actions são efêmeros, mas é boa prática.

Gerenciamento de Versão

O Android usa dois identificadores de versão: versionCode (um inteiro que deve aumentar a cada upload) e versionName (a string de versão legível por humanos).

Para projetos Flutter, ambos vêm do pubspec.yaml:

version: 1.2.3+45
# 1.2.3 = versionName
# 45 = versionCode

Eu automatizo o incremento do version code usando o número de execução do CI:

lane :set_version_code do
  build_number = ENV["GITHUB_RUN_NUMBER"] || "1"

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
  end
end

Isso garante que o version code sempre aumente, mesmo entre branches. Se você precisar de mais controle, pode buscar o último version code da Play Store:

lane :smart_version_code do
  current = google_play_track_version_codes(track: "internal").max || 0
  new_code = current + 1

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{new_code}/' pubspec.yaml")
  end
end

Gerenciando Listagens da Play Store

O Supply pode gerenciar toda a listagem da Play Store a partir de arquivos versionados:

android/fastlane/metadata/android/
├── en-US/
│   ├── title.txt
│   ├── short_description.txt
│   ├── full_description.txt
│   ├── changelogs/
│   │   ├── default.txt
│   │   └── 45.txt          # Changelog for versionCode 45
│   └── images/
│       ├── phoneScreenshots/
│       │   ├── 1_home.png
│       │   └── 2_detail.png
│       ├── featureGraphic.png
│       └── icon.png
└── ru-RU/
    ├── title.txt
    ├── short_description.txt
    └── full_description.txt

Para baixar sua listagem atual como ponto de partida:

fastlane supply init

Depois atualize os arquivos e envie as mudanças:

lane :update_listing do
  supply(
    skip_upload_aab: true,
    skip_upload_apk: true
  )
end

Configuração do ProGuard e R8

Quando minifyEnabled é true, o R8 (o sucessor do ProGuard) encolhe, ofusca e otimiza seu código. Isso é essencial para builds de produção — reduz o tamanho do APK e dificulta a engenharia reversa — mas também pode quebrar coisas se não for configurado corretamente.

Problemas comuns e suas regras ProGuard:

# Keep Flutter's classes
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }

# Keep Firebase classes
-keep class com.google.firebase.** { *; }

# Keep models used with JSON serialization
-keep class com.yourcompany.yourapp.models.** { *; }

# Keep classes referenced via reflection
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses

# Common crash fix: OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**

Teste seu build de release completamente — problemas do R8 frequentemente se manifestam como crashes em tempo de execução que não aparecem em builds de debug. As flags --obfuscate e --split-debug-info no Flutter habilitam otimizações adicionais:

flutter build appbundle --release --obfuscate --split-debug-info=build/symbols

Mantenha o diretório build/symbols — você precisa dele para simbolizar stack traces de relatórios de crash.

O Fastfile Completo

default_platform(:android)

platform :android do
  desc "Deploy to internal testing"
  lane :internal do
    set_version_code
    build_release
    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to closed beta"
  lane :beta do
    set_version_code
    build_release
    supply(
      track: "beta",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to production with staged rollout"
  lane :production do
    set_version_code
    build_release
    supply(
      track: "production",
      rollout: "0.1",
      aab: "../build/app/outputs/bundle/release/app-release.aab"
    )
  end

  desc "Promote internal to beta"
  lane :promote_to_beta do
    supply(
      track: "internal",
      track_promote_to: "beta"
    )
  end

  desc "Increase production rollout"
  lane :increase_rollout do |options|
    supply(
      track: "production",
      rollout: options[:percentage] || "0.5",
      skip_upload_aab: true
    )
  end

  private_lane :build_release do
    Dir.chdir("..") do
      sh("flutter build appbundle --release --obfuscate --split-debug-info=build/symbols")
    end
  end

  private_lane :set_version_code do
    build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
    Dir.chdir("..") do
      current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
      sh("sed -i '' 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
    end
  end
end

Diferenças da Automação iOS

Tendo automatizado ambas as plataformas, existem várias diferenças-chave que valem ser notadas.

A assinatura é mais simples no Android. Um único arquivo keystore versus a dança de certificados e provisioning profiles no iOS. Sem necessidade de match ou qualquer ferramenta de gerenciamento de certificados.

Sem requisito obrigatório de macOS. Builds Android rodam em Linux, o que significa runners de CI mais baratos e mais rápidos.

Tempos de revisão mais rápidos. O processo de revisão do Google é tipicamente mais rápido que o da Apple, e uploads para o track interno não requerem revisão alguma.

Rollouts escalonados são nativos. No iOS, você pode fazer releases faseados, mas não pode controlar a porcentagem ou pausar no meio do rollout com a mesma granularidade.

A API da Play Store é mais permissiva. Você pode gerenciar quase tudo programaticamente, incluindo experimentos de listagem na loja e preços. A API do App Store Connect da Apple tem mais restrições.

A principal desvantagem são os tempos de build do Gradle. Um build limpo do Android com otimização R8 pode levar significativamente mais tempo que um build iOS equivalente. Cache ajuda — tanto o cache nativo do Gradle quanto o cache no nível do CI do diretório .gradle.

Como um Pipeline Maduro Se Parece

Após toda a configuração, a experiência do dia a dia deve ser invisível. Push para main, um build roda, e alguns minutos depois os testadores têm uma nova versão no track interno. Marque um release, e um build de produção passa com rollout escalonado.

O pipeline remove uma categoria inteira de problemas de "funciona na minha máquina", garante que cada build é reproduzível e dá ao time confiança de que publicar é um não-evento. Essa última parte é o verdadeiro objetivo — tornar o deploy tão rotineiro que ninguém pensa sobre isso.

DU

Danil Ulmashev

Full Stack Developer

Interesse em trabalhar juntos?