Aula 31 · Semana 13 · Segunda, 30 de novembro de 2026 · 16:00 às 18:00
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.
key.properties fora do repositório e gerar um app bundle assinado.versionName de versionCode e aplicar as regras das lojas ao version do pubspec.yaml.--dart-define.~/.android/debug.keystore, gerado na primeira compilação, diferente em cada máquina.Para os testes: a assinatura prova autoria e integridade; não prova qualidade nem segurança.
| Android sem Play App Signing | Android com Play App Signing | iOS | |
|---|---|---|---|
| Quem tem a chave final | Tu, num ficheiro .jks | A Google guarda a app signing key; tu assinas com uma upload key | A Apple assina para a loja; tu tens certificados que expiram ao fim de um ano |
| Se perdes a tua chave | Nunca mais atualizas a app. Publicas outra com outro package name e perdes utilizadores, avaliações e histórico | Pedes à Google uma nova upload key e continuas | Revogas o certificado e crias outro. Incómodo, não catástrofe |
| Estado atual | Só apps antigas | Obrigatório para apps novas desde 2021 | Sempre foi assim |
.jks): ficheiro com o par de chaves, protegido por palavra-passe. Cada chave lá dentro tem um alias. Cria-se uma vez com keytool.android/key.properties: quatro linhas com o caminho do keystore, o alias e as palavras-passe. O Gradle lê-o. Vai para o .gitignore.signingConfigs em android/app/build.gradle.kts: diz ao Gradle que o build type release assina com essa chave.flutter build appbundle assina com a chave de debug e a Play Console recusa o upload.key.properties é escrito no momento do build.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 | AAB (Android App Bundle) | |
|---|---|---|
| O que é | Pacote instalável, com tudo lá dentro | Formato de publicação; a Play gera um APK à medida de cada dispositivo |
| Tamanho | Todas as arquiteturas (arm64, armv7, x86_64) e densidades | Só o que aquele telemóvel precisa: tipicamente 15 a 30 % mais pequeno |
| Onde se usa | Instalar à mão, Firebase App Distribution, testers | Obrigatório na Play Store para apps novas desde 2021 |
| Assinatura | A tua chave é a final | Assinas com a upload key; a Google volta a assinar com a app signing key |
| Comando | flutter build apk | flutter 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.
.p12.pt.uac.tempoacores). As capacidades (push, associated domains) ficam-lhe associadas..ipa.flutter build ipa gera o ficheiro .ipa; o upload faz-se com o Transporter ou com xcrun altool. Só em macOS.pubspec.yaml, como vais ver a seguir.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.
version: 1.2.3+45| No pubspec.yaml | Android | iOS | Quem o vê |
|---|---|---|---|
1.2.3 (antes do +) | versionName | CFBundleShortVersionString | O utilizador, na loja e nas definições |
45 (depois do +) | versionCode | CFBundleVersion | A loja, para ordenar builds |
flutter.versionName, flutter.versionCode) e para o Xcode (Generated.xcconfig) em cada build. O pubspec é a fonte única.--build-name=1.2.4 --build-number=46. É o que o CI faz com o número da execução.versionCode é um inteiro que tem de ser estritamente maior do que qualquer um já carregado para a app, em qualquer track. Máximo 2 100 000 000. Não se reutiliza nunca.1.2.3) o build number tem de subir; o TestFlight recusa um build repetido. Uma versão nova pode recomeçar o build em 1, mas ninguém o faz.versionCode menor do que o instalado dá INSTALL_FAILED_VERSION_DOWNGRADE. É o mesmo mecanismo que impede que um utilizador seja "atualizado" para trás.1.0.0+N, com N a subir a cada build que sai do portátil para outra pessoa.Para os testes: versionName é para humanos e pode repetir-se; versionCode é para a loja e nunca pode descer nem repetir.
productFlavors no Gradle, com applicationIdSuffix (.dev) e source sets por flavor (src/dev/res) para ícone e nome.Debug-dev, Release-prod) com ficheiros .xcconfig. Mais trabalho manual; há packages que o automatizam.flutter run --flavor dev. O nome do flavor tem de existir nas duas plataformas se compilas as duas.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--dart-define=API_URL=https://... na linha de comandos e String.fromEnvironment('API_URL') no código. É uma constante de compilação: existe também bool.fromEnvironment e int.fromEnvironment.--dart-define-from-file=env/prod.json. O ficheiro de prod vai para o .gitignore e para os secrets do CI.--obfuscate) dificulta mas não impede que alguém o extraia de um APK. Serve para configuração e chaves de baixo risco (URL, chave de mapas com restrição por package), não para segredos que dariam acesso a dados.flutter_secure_storage, como fizemos na aula 17.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.
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.
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".keytool -genkey -v -keystore ~/tempo-acores-upload.jks \
-keyalg RSA -keysize 2048 -validity 10000 -alias uploadkey.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.jksandroid/key.properties
**/*.jks
**/*.keystorekey.properties e as linhas do .gitignore. Corre git status e confirma que nenhum dos dois aparece.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"
}
}
}
import vão para o topo do ficheiro, antes de plugins { }. Corre flutter build appbundle e confirma que termina sem erros.build/app/outputs/bundle/release/app-release.aab.
flutter build appbundle --releaseflutter build apk --release
adb uninstall pt.uac.tempoacores
adb install build/app/outputs/flutter-apk/app-release.apkkeytool 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 uploadname: 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
versionCode 2 > 1 e a chave é a mesma.1.0.1+1, compila e tenta instalar por cima do 2: INSTALL_FAILED_VERSION_DOWNGRADE. Na Play Console o equivalente é "Version code 1 has already been used".flutter build apk --build-name=1.0.1 --build-number=3.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")
}
}
}
AndroidManifest.xml, android:label="@string/app_name" passa a usar o valor do flavor.android/app/src/main/res/mipmap-* para android/app/src/dev/res/mipmap-* e troca as imagens. O source set do flavor sobrepõe-se ao main.flutter run --flavor dev. As duas apps ficam instaladas ao mesmo tempo, com nomes e ícones diferentes.flutter_launcher_icons que já usaste na aula 27, apontando para assets/icon-dev.png, e instala os dois flavors lado a lado.--dart-define para o URL da APIclass 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
env/prod.json é um objeto JSON com as mesmas chaves. Entra no .gitignore; em CI vem de um secret.ApiClient passa a receber Ambiente.apiUrl. Em dev mostra o nome do ambiente na AppBar para ninguém confundir os dois.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.open ios/Runner.xcworkspace. Nunca o .xcodeproj: o workspace é o que tem os pods.pt.uac.pdm.tempoacores.grupoN. O Xcode cria o App ID e o perfil sozinho.flutter run. No telemóvel, Definições → Geral → VPN e gestão de dispositivos → confiar no programador.flutter build ipa só com conta paga.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.version: 1.0.0+1 no pubspec.yaml, keystore do grupo guardado no gestor de palavras-passe e key.properties fora do repositório.--dart-define.