PDM · DAW 2

Aula 31 · Semana 13 · Segunda, 30 de novembro de 2026 · 16:00 às 18:00

Build e assinatura

Teoria: assinatura de aplicações no Android e no iOS, versionamento aceite pelas lojas, flavors e ambientes, segredos de build. Prática: build de release assinado com uma chave do grupo, app bundle, versionamento e flavors dev e prod.

Repo: pdm-aula-31

No fim desta aula consegues

Assinatura: porque existe (1 de 2)

Para os testes: a assinatura prova autoria e integridade; não prova qualidade nem segurança.

Analogia A assinatura é o selo de um notário. Prova quem fez a app (a mesma chave é o mesmo autor) e que ninguém a alterou pelo caminho (o hash bate certo). Não diz nada sobre o conteúdo do documento: um contrato mau com selo continua a ser um contrato mau.

Assinatura: perder a chave (2 de 2)

Android sem Play App SigningAndroid com Play App SigningiOS
Quem tem a chave finalTu, num ficheiro .jksA Google guarda a app signing key; tu assinas com uma upload keyA Apple assina para a loja; tu tens certificados que expiram ao fim de um ano
Se perdes a tua chaveNunca mais atualizas a app. Publicas outra com outro package name e perdes utilizadores, avaliações e históricoPedes à Google uma nova upload key e continuasRevogas o certificado e crias outro. Incómodo, não catástrofe
Estado atualSó apps antigasObrigatório para apps novas desde 2021Sempre foi assim
Mesmo com Play App Signing O keystore e as palavras-passe vivem no gestor de palavras-passe da equipa, com cópia de segurança. Nunca no repositório, nunca só no portátil de uma pessoa.
Analogia A keystore é o próprio carimbo do notário. Quem o perde nunca mais consegue assinar uma atualização como sendo a mesma app: para o sistema, uma app com outro carimbo é outra app. O Play App Signing é a Google a guardar uma cópia do carimbo final; a tua upload key é um carimbo secundário que, se se perder, se substitui.

Android: keystore, key.properties, signingConfigs

1. Keystore (keytool) upload-keystore.jks fora do repo, com backup 2. key.properties caminho, alias e palavras-passe no .gitignore; a CI escreve-o 3. signingConfigs app/build.gradle.kts release assina com esta chave 4. Build de release flutter build appbundle sem isto: chave de debug 5. AAB assinado app-release.aab com a tua upload key 6. Play App Signing Google guarda a app signing key gera APKs por dispositivo Perder o keystore sem Play App Signing = nunca mais atualizar a app Publicas outra app e perdes utilizadores e avaliações. Keystore e palavras-passe: no gestor da equipa, com backup.
O keystore assina; o key.properties só diz onde ele está. Nenhum dos dois entra no git.

Para os testes: o keystore assina; o key.properties só diz onde ele está e como abri-lo. Nenhum dos dois entra no git.

APK vs AAB

APKAAB (Android App Bundle)
O que éPacote instalável, com tudo lá dentroFormato de publicação; a Play gera um APK à medida de cada dispositivo
TamanhoTodas as arquiteturas (arm64, armv7, x86_64) e densidadesSó o que aquele telemóvel precisa: tipicamente 15 a 30 % mais pequeno
Onde se usaInstalar à mão, Firebase App Distribution, testersObrigatório na Play Store para apps novas desde 2021
AssinaturaA tua chave é a finalAssinas com a upload key; a Google volta a assinar com a app signing key
Comandoflutter build apkflutter build appbundle

Um AAB não se instala com adb install. Para testar localmente a partir de um bundle usa-se o bundletool da Google, ou gera-se também um APK.

iOS: certificados e provisioning profiles (1 de 2)

Apple Developer (developer.apple.com) Certificado (identidade) pedido por CSR; válido um ano App ID (bundle id) pt.uac.tempoacores Dispositivos (UDID) só em desenvolvimento e ad hoc Provisioning profile = certificado + App ID + dispositivos + entitlements Xcode assina automatic signing trata disto flutter build ipa IPA assinado perfil embutido; upload: Transporter Perfil de desenvolvimento correr no teu iPhone; só nos dispositivos listados Perfil de distribuição TestFlight e App Store; por equipa; conta paga
Certificado, App ID e dispositivos juntam-se num provisioning profile; é com ele que o Xcode assina o IPA.
Analogia No iOS há ainda o registo notarial da Apple. O certificado é a identidade do notário (quem és); o provisioning profile é o registo de em que dispositivos e para que app o selo vale. Sem os dois o Xcode não assina, e o selo final que chega à loja é o da própria Apple.

iOS: bundle id e automatic signing (2 de 2)

Para os testes: no iOS quem assina a app que chega à loja é a Apple; o teu certificado de distribuição só te autoriza a fazer o upload.

Versionamento: version: 1.2.3+45

No pubspec.yamlAndroidiOSQuem o vê
1.2.3 (antes do +)versionNameCFBundleShortVersionStringO utilizador, na loja e nas definições
45 (depois do +)versionCodeCFBundleVersionA loja, para ordenar builds

Versionamento: regras das lojas

Para os testes: versionName é para humanos e pode repetir-se; versionCode é para a loja e nunca pode descer nem repetir.

Flavors e ambientes

Para os testes: um flavor muda a identidade da app (id, nome, ícone) e a configuração de build; não muda o código Dart.

--dart-define para configuração e segredos de build

Para os testes: --dart-define injeta valores em tempo de compilação; tudo o que entra no binário pode ser lido por quem o descompilar.

Assinar, versionar e separar ambientes

git clone https://github.com/pdm-daw-2026/pdm-aula-31.git
cd pdm-aula-31/starter
flutter pub get
flutter run

starter/ é a app Tempo Açores da aula 28 com TODOs numerados no build.gradle.kts, no pubspec.yaml e em lib/config/ambiente.dart. solucao/ tem o estado final, sem o keystore, claro.

Antes de começar Confirma que keytool responde no terminal. Se não, usa o caminho completo: em macOS e Linux $(/usr/libexec/java_home)/bin/keytool ou o JDK do Android Studio; em Windows "C:\Program Files\Android\Android Studio\jbr\bin\keytool.exe".

1. Criar o keystore e o key.properties

  1. Gera a chave, fora da pasta do projeto. Guarda as duas palavras-passe no gestor de palavras-passe do grupo.
    keytool -genkey -v -keystore ~/tempo-acores-upload.jks \
      -keyalg RSA -keysize 2048 -validity 10000 -alias upload
  2. Escreve o key.properties dentro de android/, com o caminho absoluto do keystore.
    storePassword=a-tua-palavra-passe
    keyPassword=a-tua-palavra-passe
    keyAlias=upload
    storeFile=/Users/eu/tempo-acores-upload.jks
  3. Protege os dois no git antes de fazeres qualquer commit.
    android/key.properties
    **/*.jks
    **/*.keystore
Exercício 1 Cria o keystore, o key.properties e as linhas do .gitignore. Corre git status e confirma que nenhum dos dois aparece.

2. signingConfigs no build.gradle.kts

import java.util.Properties
import java.io.FileInputStream

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // ...namespace, compileSdk, defaultConfig ficam como estão
    signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")   // TODO 1: era "debug"
        }
    }
}
Exercício 2 Aplica o TODO 1. Os import vão para o topo do ficheiro, antes de plugins { }. Corre flutter build appbundle e confirma que termina sem erros.

3. Gerar, instalar e verificar o release

  1. App bundle para a loja. Fica em build/app/outputs/bundle/release/app-release.aab.
    flutter build appbundle --release
  2. APK de release para instalar no telemóvel. Desinstala primeiro a versão de debug: a chave é outra e o Android recusa a atualização.
    flutter build apk --release
    adb uninstall pt.uac.tempoacores
    adb install build/app/outputs/flutter-apk/app-release.apk
  3. Verifica quem assinou. O keytool lê o certificado de dentro do APK. Compara o SHA-256 com o do teu keystore.
    keytool -printcert -jarfile build/app/outputs/flutter-apk/app-release.apk
    keytool -list -v -keystore ~/tempo-acores-upload.jks -alias upload
Exercício 3 Instala o APK de release no telemóvel de um colega de outro grupo e mostra-lhe o SHA-256 do certificado. Repara que o release arranca mais depressa e não tem o banner de debug.

4. Subir a versão, e o que acontece se não subires

name: tempo_acores
description: Previsão meteorológica para os Açores.
publish_to: 'none'

version: 1.0.1+2   # TODO 2: era 1.0.0+1

environment:
  sdk: ^3.5.0
Exercício 4 Faz as três experiências acima e anota a mensagem de erro exata do downgrade. Vai aparecer-te na vida real quando o CI reutilizar um número de build.

5. Flavors dev e prod no Android

android {
    // ...
    flavorDimensions += "ambiente"
    productFlavors {
        create("dev") {
            dimension = "ambiente"
            applicationIdSuffix = ".dev"          // pt.uac.tempoacores.dev
            versionNameSuffix = "-dev"
            resValue("string", "app_name", "Tempo Açores DEV")
        }
        create("prod") {
            dimension = "ambiente"
            resValue("string", "app_name", "Tempo Açores")
        }
    }
}
Exercício 5 Aplica o TODO 3 (flavors) e o TODO 4 (label no manifesto). Gera o ícone de dev com o flutter_launcher_icons que já usaste na aula 27, apontando para assets/icon-dev.png, e instala os dois flavors lado a lado.

6. --dart-define para o URL da API

class Ambiente {
  // TODO 5: substituir o URL fixo por String.fromEnvironment
  static const apiUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'https://api.pdm-daw.pt/v1',
  );
  static const nome = String.fromEnvironment('AMBIENTE', defaultValue: 'prod');
  static const isDev = nome == 'dev';
}
flutter run --flavor dev \
  --dart-define=AMBIENTE=dev --dart-define=API_URL=https://api-dev.pdm-daw.pt/v1
flutter build appbundle --flavor prod --dart-define-from-file=env/prod.json
Exercício 6 Aplica o TODO 5 e o TODO 6 (banner "DEV" na AppBar quando Ambiente.isDev). Corre o flavor dev com um URL inventado e confirma que o ecrã de erro da aula 14 aparece com a mensagem certa.

7. iOS, para quem tem Mac

  1. Abre o projeto no Xcode com open ios/Runner.xcworkspace. Nunca o .xcodeproj: o workspace é o que tem os pods.
  2. Runner → Signing & Capabilities. Marca Automatically manage signing e escolhe a tua equipa. Sem conta paga aparece "Personal Team" com o teu Apple ID.
  3. Bundle identifier único: pt.uac.pdm.tempoacores.grupoN. O Xcode cria o App ID e o perfil sozinho.
  4. Liga o iPhone e corre flutter run. No telemóvel, Definições → Geral → VPN e gestão de dispositivos → confiar no programador.
  5. Limites da conta gratuita: o perfil expira em 7 dias, máximo 3 apps, sem push e sem TestFlight. flutter build ipa só com conta paga.
Exercício 7 Quem tem Mac e iPhone: app a correr no iPhone com automatic signing. Quem tem Mac sem iPhone: flutter run no simulador não precisa de assinatura, mas faz na mesma os passos 1 a 3 para veres o que o Xcode gera.

Até à próxima aula

Para fazer

  • Build de release assinado do projeto, com version: 1.0.0+1 no pubspec.yaml, keystore do grupo guardado no gestor de palavras-passe e key.properties fora do repositório.
  • Flavor dev e prod no projeto, com o URL da API por --dart-define.
  • Quarta é a review da sprint 3, às 16:30. Demo de 5 minutos preparada.

Para ler