Flutter: Implantação do Projeto
Lançamento não é o fim — é o ponto de partida da entrega contínua.
📋 Pré-requisitos: Você deve ter dominado o seguinte primeiro
1. O Que Você Vai Aprender
- Otimização de build: Tree Shaking / code splitting / compressão de assets / ofuscação R8
- Implantação Android: chave de assinatura + App Bundle + publicação no Google Play
- Implantação iOS: configuração de certificado + TestFlight + revisão da App Store
- Implantação Web: Firebase Hosting + PWA + otimização SEO
- Monitoramento de operações: coleta de crashes Crashlytics + rastreamento de rede Performance + distribuição App Center
2. Uma História de Crash no Dia do Lançamento
(1) A Dor: Funciona no Desenvolvimento, 5% de Taxa de Crash em Produção
Bob gastou 4 meses desenvolvendo o ShopApp. No dia do lançamento, a taxa de crash foi de 5%. Causas: a ofuscação R8 do Android removeu nomes de classes de serialização do Firestore (desserialização falhou); o iOS foi rejeitado por falta de descrição de permissão da câmera; a homepage da versão Web levou 8 segundos para carregar (sem code splitting). Correções de emergência + nova revisão levaram mais 2 semanas.
(2) Solução com Otimização de Build + Checklist Pré-release
Antes do lançamento, execute otimizações de build (regras de ofuscação, code splitting, compressão de assets) + um checklist de release (permissões, assinatura, regras ProGuard, pré-revisão TestFlight) para garantir qualidade no primeiro lançamento.
(3) O Resultado: Taxa de Crash no Primeiro Lançamento < 0.1%
Depois de usar o checklist em seu segundo projeto, Bob alcançou uma taxa de crash no primeiro lançamento de 0.08%, passou na revisão na primeira submissão, e teve um first-contentful paint Web de 1.5 segundos.
3. Otimização de Build
(1) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Regras Android ProGuard / R8
# flutter_proguard_rules.pro
# Manter classes de modelo do Firestore (usadas por json_serializable)
-keepclassmembers class com.shopapp..model. {
<fields>;
}
# Manter classes geradas pelo freezed
-keep class com.shopapp...freezed.dart { *; }
# Manter classes do Firebase
-keep class io.flutter.plugins.firebase.** { *; }
-keep class com.google.firebase.** { *; }
# Manter Stripe SDK
-keep class com.stripe.android.** { *; }
# Manter Google Sign-In
-keep class com.google.android.gms.auth.** { *; }
# Remover logging em release
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(2) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Configuração de build do Flutter
# pubspec.yaml build optimization
flutter:
uses-material-design: true
# Excluir locales não utilizados
generate: true
# android/app/build.gradle
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
// Split por ABI para reduzir tamanho do APK
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
universalApk false
}
}
}
(3) Comparação de Otimização de Build
| Otimização | Antes | Depois | Economia |
|---|---|---|---|
| Tamanho APK (arm64) | 28 MB | 12 MB | 57% |
| Web first-contentful paint | 8.2 s | 1.5 s | 82% |
| Tamanho do código Dart | 6.4 MB | 2.1 MB | 67% |
| Tempo de inicialização (frio) | 3.5 s | 1.8 s | 49% |
4. Implantação Android
(1) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Configuração de chave de assinatura
# Gerar keystore de assinatura
keytool -genkey -v -keystore shopapp-release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias shopapp
# Criar key.properties
cat > android/key.properties << 'EOF'
storePassword=STORE_PASSWORD
keyPassword=KEY_PASSWORD
keyAlias=shopapp
storeFile=../shopapp-release.jks
EOF
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(2) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Integração de assinatura no build.gradle
// android/app/build.gradle
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Build App Bundle e upload
# Build App Bundle (recomendado sobre APK para Play Store)
flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info
# Upload para o Google Play Console
# Opção 1: Manual via UI do Play Console
# Opção 2: Fastlane
cd android && fastlane supply --aab ../build/app/outputs/bundle/release/app-release.aab --track internal
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(4) Checklist de Release Android
| # | Item de Verificação | Status |
|---|---|---|
| 1 | Chave de assinatura gerada e com backup | ☐ |
| 2 | key.properties não está no controle de versão | ☐ |
| 3 | Regras ProGuard cobrem todos os SDKs de terceiros | ☐ |
| 4 | versionCode / versionName atualizados | ☐ |
| 5 | Declarações de permissões minimizadas | ☐ |
| 6 | App Bundle (não APK) para upload | ☐ |
| 7 | Screenshots da loja + descrição preparados | ☐ |
| 8 | URL da Política de Privacidade configurada | ☐ |
5. Implantação iOS
(1) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Configuração de permissões no Info.plist
<!-- ios/Runner/Info.plist -->
`<key>`NSCameraUsageDescription</key>
`<string>`ShopApp precisa de acesso à câmera para escanear códigos de barras de produtos.</string>
`<key>`NSPhotoLibraryUsageDescription</key>
`<string>`ShopApp precisa de acesso às fotos para fazer upload de avaliações de produtos.</string>
`<key>`NSLocationWhenInUseUsageDescription</key>
`<string>`ShopApp usa localização para estimativas de entrega local.</string>
`<key>`CFBundleURLTypes</key>
`<array>`
`<dict>`
`<key>`CFBundleURLSchemes</key>
`<array>`
`<string>`com.googleusercontent.apps.YOUR_CLIENT_ID</string>
</array>
`</dict>`
</array>
(2) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Release iOS com Fastlane
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
include_bitcode: false,
export_options: {
compileBitcode: false,
provisioningProfiles: {
"com.shopapp.app" => "match AppStore com.shopapp.app"
}
}
)
upload_to_testflight(
skip_waiting_for_build_processing: true
)
end
desc "Submit to App Store review"
lane :release do
beta
deliver(
submit_for_review: true,
automatic_release: true,
force: true,
metadata_path: "./metadata"
)
end
end
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Motivos Comuns de Rejeição na Revisão iOS
| Motivo de Rejeição | Solução |
|---|---|
| Descrições de permissão ausentes | Adicionar NS*UsageDescription ao Info.plist |
| Pagamento não usa IAP (bens virtuais) | Usar StoreKit para bens virtuais, Stripe para físicos |
| Login força vinculação de conta social | Fornecer opção de login email/senha |
| App wrapper de WebView | Garantir funcionalidade nativa > 50% |
| Metadados incompletos | Preencher classificação etária, URL da política de privacidade |
6. Implantação Web
(1) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Configuração do Firebase Hosting
// firebase.json
{
"hosting": {
"public": "build/web",
"ignore": ["firebase.json", "/.*", "/node_modules/**"],
"rewrites": [
{ "source": "**", "destination": "/index.html" }
],
"headers": [
{
"source": "**/*.@(js|css|wasm)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "**/*.@(jpg|jpeg|gif|png|svg|webp|ico)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=604800" }
]
},
{
"source": "/index.html",
"headers": [
{ "key": "Cache-Control", "value": "no-cache" }
]
}
]
}
}
(2) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Build e deploy Web
# Build web com renderizador CanvasKit (melhor desempenho)
flutter build web --release --web-renderer canvaskit --pwa-strategy offline_first
# Deploy para Firebase Hosting
firebase deploy --only hosting
# Ou deploy para servidor personalizado com Nginx
scp -r build/web/* user@server:/var/www/shopapp/
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Comparação de Otimização de Desempenho Web
| Estratégia de Otimização | First Paint | LCP | FID |
|---|---|---|---|
| Sem otimização (renderizador HTML) | 8.2 s | 7.8 s | 180 ms |
| Renderizador CanvasKit | 3.1 s | 2.8 s | 45 ms |
| + Code split + lazy | 1.5 s | 1.2 s | 30 ms |
| + PWA + Service Worker | 0.8 s (repetido) | 0.5 s | 15 ms |
7. Monitoramento de Operações
(1) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Integração Crashlytics
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:ui' show PlatformDispatcher;
// Dependências: firebase_core: ^2.0.0, firebase_crashlytics: ^3.0.0, firebase_auth: ^4.0.0
// DefaultFirebaseOptions / ShopApp vêm do projeto ShopApp
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Crashlytics
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
// Passar todos os erros não capturados para Crashlytics
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
// Definir identificador de usuário para relatórios de crash
FirebaseAuth.instance.authStateChanges().listen((user) {
if (user != null) {
FirebaseCrashlytics.instance.setUserIdentifier(user.uid);
}
});
// Chaves personalizadas para contexto
FirebaseCrashlytics.instance.setCustomKey('app_version', '1.0.0');
FirebaseCrashlytics.instance.setCustomKey('environment', 'production');
runApp(const ShopApp());
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(2) ▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Monitoramento de Desempenho
import 'package:dio/dio.dart';
import 'package:firebase_performance/firebase_performance.dart';
// Dependências: firebase_performance: ^0.9.0, dio: ^5.0.0 (adicione às dependências)
class ShopPerformance {
static final _perf = FirebasePerformance.instance;
static Future<void> trackProductLoad(String productId) async {
final trace = _perf.newTrace('product_load');
trace.putAttribute('product_id', productId);
await trace.start();
try {
// ... carregar produto ...
} finally {
await trace.stop();
}
}
static Future<void> trackSearch(String query, int results) async {
final trace = _perf.newTrace('search');
trace.putAttribute('query', query);
trace.putMetric('results_count', results);
await trace.start();
await trace.stop();
}
static Future<void> trackCheckout(double total) async {
final trace = _perf.newTrace('checkout');
trace.putMetric('order_total_cents', (total * 100).toInt());
await trace.start();
await trace.stop();
}
}
// Rastreamento automático de desempenho HTTP com Dio
class PerformanceInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.extra['trace'] = FirebasePerformance.instance.newHttpMetric(
options.uri.toString(), HttpMethod.Post);
(options.extra['trace'] as HttpMetric).start();
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final trace = response.requestOptions.extra['trace'] as HttpMetric?;
trace?.httpResponseCode = response.statusCode;
trace?.stop();
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final trace = err.requestOptions.extra['trace'] as HttpMetric?;
trace?.httpResponseCode = err.response?.statusCode;
trace?.stop();
handler.next(err);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
8. Exemplo Completo: Script Automatizado de Verificação Pré-release
// tool/release_check.dart
import 'dart:io';
// Script Dart puro, sem dependência Flutter, execute com dart run tool/release_check.dart
enum Platform { android, ios, web }
class ReleaseChecker {
static Future<bool> run(Platform platform) async {
final results = <String, bool>{};
results['version_updated'] = _checkVersionUpdated();
results['changelog_updated'] = _checkChangelog();
results['no_debug_print'] = _checkNoDebugPrint();
results['proguard_rules'] = platform == Platform.android ? _checkProguard() : true;
results['info_plist'] = platform == Platform.ios ? _checkInfoPlist() : true;
results['privacy_policy'] = _checkPrivacyPolicy();
results['signing_config'] = _checkSigningConfig(platform);
results['test_pass'] = await _runTests();
results['analyze_pass'] = await _runAnalyze();
final allPass = results.values.every((v) => v);
if (!allPass) {
print('Release check FALHOU:');
results.forEach((k, v) { if (!v) print(' ✗ $k'); });
} else {
print('Todas as verificações de release PASSARAM!');
}
return allPass;
}
static bool _checkVersionUpdated() {
// Comparar versão atual do pubspec.yaml com a última git tag
return true; // Simplificado
}
static bool _checkChangelog() => File('CHANGELOG.md').existsSync();
static bool _checkNoDebugPrint() {
// Grep por debugPrint / print() em lib/
return true; // Simplificado
}
static bool _checkProguard() => File('android/app/proguard-rules.pro').existsSync();
static bool _checkInfoPlist() {
final plist = File('ios/Runner/Info.plist').readAsStringSync();
return plist.contains('NSCameraUsageDescription') && plist.contains('NSPhotoLibraryUsageDescription');
}
static bool _checkPrivacyPolicy() => true;
static bool _checkSigningConfig(Platform platform) {
if (platform == Platform.android) return File('android/key.properties').existsSync();
return true; // iOS usa assinatura gerenciada pelo Xcode
}
static Future<bool> _runTests() async {
final result = await Process.run('flutter', ['test']);
return result.exitCode == 0;
}
static Future<bool> _runAnalyze() async {
final result = await Process.run('flutter', ['analyze']);
return result.exitCode == 0;
}
}
void main(List<String> args) async {
final platform = switch (args.firstOrNull) {
'android' => Platform.android,
'ios' => Platform.ios,
'web' => Platform.web,
_ => Platform.android,
};
final pass = await ReleaseChecker.run(platform);
exit(pass ? 0 : 1);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
9. Visão Geral do Processo de Implantação
graph TD
CODE[Source Code] --> ANALYZE[flutter analyze]
ANALYZE --> TEST[flutter test]
TEST --> CHECK{Release Check}
CHECK -->|FAIL| FIX[Corrigir Problemas]
FIX --> ANALYZE
CHECK -->|PASS| BUILD[flutter build]
BUILD --> ANDROID[Android: AAB → Play Console]
BUILD --> IOS[iOS: IPA → TestFlight → App Store]
BUILD --> WEB[Web: Firebase Hosting]
ANDROID --> MONITOR[Crashlytics + Performance]
IOS --> MONITOR
WEB --> MONITOR
MONITOR --> ITERATE[Iterar e Lançar Próxima Versão]
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: Qual a diferença entre App Bundle e APK? R: AAB é o formato de publicação do Google Play que automaticamente divide por ABI/densidade do dispositivo para menor tamanho de download. APK é o pacote de instalação completo usado para sideloading.
P: Quanto tempo leva a revisão iOS geralmente? R: Primeira submissão 2-5 dias, atualizações subsequentes 1-2 dias. Após rejeição, corrigir e reenviar leva mais 1-3 dias.
P: O Flutter Web suporta SEO? R: O Flutter Web é um SPA — mecanismos de busca têm dificuldade em indexar conteúdo. Soluções: 1) Usar SSR (dart2js + servidor); 2) Pré-renderizar páginas chave; 3) Complementar com landing pages estáticas.
P: E se as stacks do Crashlytics ficarem ilegíveis após ofuscação? R: Use --split-debug-info durante builds para salvar tabelas de símbolos, então faça upload dos arquivos de desobfuscação (Android) ou dSYMs (iOS) para o Crashlytics para restaurar stack traces.
P: Hot update / Code Push suporta Flutter? R: Flutter não suporta nativamente hot updates de código. Você pode usar shorebird.dev para pushes parciais de código (Android), mas iOS ainda requer revisão da App Store.
P: Como gerencio versões em releases multiplataforma simultâneos? R: Use uma versão unificada no pubspec.yaml, com builds de matriz CI/CD (GitHub Actions: jobs android + ios + web em paralelo).
P: Qual taxa de crash é considerada normal após o lançamento? R: Excelente < 0.1%, Boa < 0.5%, Precisa de atenção > 1%. Crashlytics fornece um dashboard de taxa de crash em tempo real.
📖 Resumo
- Otimização de build: ofuscação R8 + split por ABI + renderizador CanvasKit — APK reduzido em 57%, Web first paint reduzido em 82%
- Android: chave de assinatura → App Bundle → Play Console → rollout interno/beta/produção
- iOS: permissões Info.plist → Fastlane → TestFlight → revisão App Store
- Web: Firebase Hosting + PWA + Service Worker + estratégia de cache CDN
- Operações: coleta de crashes Crashlytics + rastreamento de rede Performance + verificações de release automatizadas
📝 Exercícios
- Básico (dificuldade ⭐): Configure chave de assinatura Android + regras ProGuard, faça build de um Release App Bundle, e verifique a otimização de tamanho.
- Intermediário (dificuldade ⭐⭐): Configure Crashlytics + Monitoramento de Performance, simule um crash e uma requisição lenta, e verifique o relatório de dados de monitoramento.
- Desafio (dificuldade ⭐⭐⭐): Escreva um pipeline CI/CD completo (GitHub Actions): analyze → test → release check → build (android + ios + web) → deploy, alcançando release multiplataforma com um clique.