Mert Özen Her Satırda Daha İleri
Frontend Flutter ile Mobil — #16

Flutter Uygulamasını Yayınlamak: Google Play ve App Store, Signing, Versioning, Flavor'lar ve CI

Mert Özen 15 Kas 2026 8 dk 4 görüntülenme
Flutter Uygulamasını Yayınlamak: Google Play ve App Store, Signing, Versioning, Flavor'lar ve CI

Flutter uygulaması iki mağazada da aynı yoldan geçer: release build, imzalama, test kanalı, inceleme ve kademeli yayın. Bookstore için sürüm numaralarını, --dart-define-from-file ile ortamları (ve neden secret taşıyamadıklarını), Android upload key ve Play App Signing'i, iOS imzalama ve TestFlight'ı, 2026 mağaza kurallarını ve GitHub Actions'ta imzalı App Bundle build'ini anlatıyoruz.

Sürüm notu: Bu yazıdaki örnekler Flutter 3.47.8 (Dart 3.13.7) ile denenmiştir; çıktılar flutter test, flutter build web ve actionlint 1.7.12 koşularından. Android App Bundle build'i, imzalama, iOS build'i, TestFlight, Play Console ve App Store Connect adımları cihaz, imzalama anahtarı ve mağaza hesabı gerektirdiği için çalıştırılmadı; Flutter, Google Play ve Apple dokümanlarına göre anlatılıyor. Mağaza kuralları 15 Kasım 2026'da ilgili sayfalardan tekrar kontrol edildi.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) staging build'ini --dart-define-from-file=config/staging.json ile alıyorsun. Ekipten biri aynı dosyaya bir ödeme servisinin API anahtarını eklemeyi öneriyor: "nasılsa --obfuscate ile build ediyoruz, kimse okuyamaz". Haklı mı? Obfuscation, binary'deki string'leri gizler mi?

Koddan Mağazaya ve Sürüm Numaraları

Her iki platformda yol aynı: release build, imzalama, bir test kanalı, mağaza incelemesi ve yayın. Değişen, her adımın aracı:

16-publishing-flow-tr.png

Sürüm bilgisi tek yerde, pubspec.yaml'da: version: 1.0.0+1. +'dan önceki kısım kullanıcının gördüğü sürüm (Android'de versionName, iOS'ta CFBundleShortVersionString), sonraki kısım build numarası (versionCode, CFBundleVersion). Mağazalar her yüklemede yeni bir build numarası istiyor; bu yüzden CI'da pubspec'i değiştirmek yerine build sırasında --build-name ve --build-number veriliyor. Web build'inde bunu doğrudan görebiliyoruz:

flutter build web --release --dart-define-from-file=config/staging.json --build-name=1.1.0 --build-number=7
build/web/version.json:
{"app_name":"bookstore_app","version":"1.1.0","build_number":"7","package_name":"bookstore_app"}

Ortamlar: --dart-define-from-file ya da Flavor

Bookstore'un üç ortamı var: geliştirme makinesindeki API, staging ve production. Her biri için bir JSON dosyası:

{
  "APP_ENV": "staging",
  "API_BASE_URL": "https://staging-api.bookstore.example.com"
}
/// Build anındaki ayarlar, her ortam için bir dosya:
/// flutter build appbundle --dart-define-from-file=config/prod.json
/// Bu değerler uygulama binary'sinin içine girer: buraya asla secret koyma.
const apiBaseUrl = String.fromEnvironment(
  'API_BASE_URL',
  defaultValue: 'http://127.0.0.1:5080',
);

/// dev, staging ya da prod; prod dışındaki her ortam köşede banner gösterir.
const appEnvironment = String.fromEnvironment('APP_ENV', defaultValue: 'dev');

BookstoreApp, prod dışındaki build'lerde MaterialApp.builder ile sol üst köşeye ortamın adını yazan bir Banner koyuyor; test eden kişi staging'i gerçek uygulamayla karıştırmasın. Aynı test üç dosyayla:

APP_ENV=dev, API_BASE_URL=http://127.0.0.1:5080, banners: DEBUG, DEV
APP_ENV=staging, API_BASE_URL=https://staging-api.bookstore.example.com, banners: DEBUG, STAGING
APP_ENV=prod, API_BASE_URL=https://api.bookstore.example.com, banners: DEBUG

DEBUG banner'ı MaterialApp'in debug build'lerde koyduğu banner; release build'de yok. "Önce Tahmin Et"in cevabı staging web build'inin çıktısında:

grep -o "https://staging-api.bookstore.example.com" build/web/main.dart.js | sort | uniq -c
   1 https://staging-api.bookstore.example.com

Değer derlenmiş JavaScript'in içinde düz metin olarak duruyor. Android ve iOS'ta da durum farklı değil: Flutter'ın obfuscation dokümanı, obfuscation'ın yalnız sembol adlarını değiştirdiğini, kaynakları şifrelemediğini ve uygulamaya secret konmaması gerektiğini açıkça yazıyor. Ödeme anahtarı gibi bir secret sunucuda kalır; uygulama yalnız kendi API'sini çağırır.

İkinci seçenek native flavor'lar: Android'de productFlavors, iOS'ta ayrı Xcode scheme'leri ve flutter run --flavor staging. Karşılaştırma basit. --dart-define-from-file her platformda aynı çalışıyor, native proje dosyalarına dokunmuyor ama tek bir uygulama kimliği üretiyor. Flavor'lar farklı applicationId, ad ve ikonla staging ile production'ı aynı telefona yan yana kurmayı sağlıyor, karşılığında Gradle ve Xcode tarafında ayar ve her flavor için ayrı imzalama gerektiriyor. Bookstore şimdilik birincisiyle ilerliyor; iki uygulamanın yan yana kurulması gerekirse flavor eklemek, config dosyalarını bozmadan yapılabilir.

Android: App Bundle, Upload Key ve Play App Signing

Google Play APK (Android uygulama paketi dosyası) değil App Bundle (.aab) istiyor; Play, bundle'dan her cihaza uygun APK'yı kendisi üretiyor. İmzalama iki anahtarla çalışıyor: senin elindeki upload key ile bundle'ı imzalayıp yüklüyorsun, Play App Signing kullanıcıya giden APK'ları Google'ın sakladığı app signing key ile imzalıyor. Upload key kaybolursa Play Console'dan sıfırlatılabiliyor; app signing key'in kaybolması ise uygulamanın güncellenememesi demek, bu yüzden Google'da durması iyi bir şey. Flutter dokümanındaki komutla anahtar oluşturuluyor ve bilgileri repoya girmeyen bir dosyaya yazılıyor:

keytool -genkey -v -keystore $env:USERPROFILE\upload-keystore.jks `
        -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 `
        -alias upload
android/key.properties:
storePassword=<password-from-previous-step>
keyPassword=<password-from-previous-step>
keyAlias=upload
storeFile=<keystore-file-location>

Flutter şablonunun android/.gitignore'u key.properties, *.jks ve *.keystore'u zaten dışarıda bırakıyor. build.gradle.kts dosyayı varsa okuyor; yeni bir clone'da dosya olmadığı için release build debug anahtarıyla imzalanıyor ve geliştirici takılmıyor:

// Google Play için upload key; key.properties ve .jks dosyası git'e girmez
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
buildTypes {
    release {
        // key.properties yoksa (yeni bir clone) release build debug anahtarını kullanır
        signingConfig = if (keystorePropertiesFile.exists()) {
            signingConfigs.getByName("release")
        } else {
            signingConfigs.getByName("debug")
        }
    }
}

Bu Gradle değişikliğini Android build'i çalıştırmadığımız için derlemedik; kod Flutter dokümanındaki örneğin, dosya yokken debug anahtarına düşen hali. Release build'lerde R8 (Android'in kod küçültme ve sadeleştirme aracı) varsayılan olarak açık. Dart kodunu obfuscate etmek için --obfuscate --split-debug-info=build/symbols veriliyor; bu klasöre yazılan sembol dosyalarını sakla, çünkü üretimden gelen bir stack trace'i flutter symbolize ile okunur hale getirmenin tek yolu onlar. Dokümana göre web build'leri obfuscation desteklemiyor, onun yerine minify ediliyor.

Play'in target API kuralı: 31 Ağustos 2026'dan beri yeni uygulamalar ve güncellemeler Android 16'yı (API 36) hedeflemek zorunda; ek süre isteyenler için 1 Kasım 2026'ya kadar uzatma vardı. Flutter, targetSdk'yı flutter.targetSdkVersion ile kendi değeriyle dolduruyor; Flutter sürümünü güncel tutmak bu kuralın büyük kısmını karşılıyor. İlk yükleme internal testing'e (hızlı, küçük test grubu) gider. Google'ın yardım sayfasına göre 13 Kasım 2023'ten sonra açılmış kişisel geliştirici hesaplarında production'a çıkmadan önce en az 12 test kullanıcısıyla 14 gün kesintisiz kapalı test yapmak gerekiyor; bunu takvime baştan koy.

iOS: Bundle ID, İmzalama ve TestFlight

iOS tarafı macOS ve Xcode gerektiriyor; Windows'ta çalışan bir ekip için bu adım ya bir Mac'te ya da macOS runner'lı bir CI'da yapılır. Sıra Flutter dokümanına göre şöyle: Apple Developer Program üyeliği, geliştirici hesabında açık bir Bundle ID (ör. com.example.bookstore), App Store Connect'te o Bundle ID'ye bağlı bir uygulama kaydı ve Xcode'da Runner target'ı için "Automatically manage signing" ile takım seçimi. Sonra:

flutter build ipa --release --dart-define-from-file=config/prod.json --obfuscate --split-debug-info=build/symbols

Komut build/ios/archive/'a bir .xcarchive, build/ios/ipa'ya bir .ipa yazıyor. Yükleme Apple'ın Transporter uygulamasıyla, xcrun altool ile ya da Xcode'un "Distribute App" adımıyla yapılıyor. Build App Store Connect'te işlendikten sonra TestFlight'ta iç test kullanıcılarına açılabiliyor; dış test kullanıcıları için Apple'ın beta incelemesi gerekiyor. Apple'ın "upcoming requirements" sayfasına göre 28 Nisan 2026'dan beri yüklemeler Xcode 26 ve iOS 26 SDK ile build edilmeli, 9 Eylül 2026'dan beri de en az iOS 13'ü hedeflemeli.

App Review'da Bookstore gibi bir uygulamanın en sık takıldığı yerler: gizlilik politikası adresi ve App Store Connect'teki gizlilik bilgileri (uygulamanın ve kullandığı SDK'ların topladığı veri), uygulamanın içinden hesap açılabiliyorsa hesabı uygulamanın içinden silme imkânı (Google Play'in de benzer bir hesap silme kuralı var), ve inceleme ekibi için çalışan bir test hesabı. Giriş gerektiren her ekranı reviewer'ın açabilmesi gerekiyor.

CI: GitHub Actions'ta Test, Build ve İmzalama

Workflow'un genel yapısı GitHub Actions serisinin konusu; burada yalnız mobil adımlar var. Her pull request'te analyze ve testler, mobile-v* tag'inde imzalı App Bundle:

- name: Restore the upload key from secrets
  env:
    UPLOAD_KEYSTORE_BASE64: ${{ secrets.UPLOAD_KEYSTORE_BASE64 }}
    UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
  run: |
    echo "$UPLOAD_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks"
    {
      echo "storePassword=$UPLOAD_KEYSTORE_PASSWORD"
      echo "keyPassword=$UPLOAD_KEYSTORE_PASSWORD"
      echo "keyAlias=upload"
      echo "storeFile=$RUNNER_TEMP/upload-keystore.jks"
    } > android/key.properties
- name: Build the app bundle
  env:
    BUILD_NUMBER: ${{ github.run_number }}
  run: >
    flutter build appbundle --release
    --dart-define-from-file=config/prod.json
    --build-number="$BUILD_NUMBER"
    --obfuscate --split-debug-info=build/symbols

Upload key repoda değil, base64 olarak bir GitHub secret'ında; job play-internal environment'ına bağlı, böylece secret'lar yalnız onaylı tag build'lerinde açılıyor. Action'lar commit hash'iyle (SHA) sabitlendi, actions/checkout token'ı git ayarlarında bırakmıyor (persist-credentials: false) ve build numarası github.run_number'dan geliyor. Workflow'u çalıştıramadık (repo ve secret yok), ama actionlint'ten geçirdik; çıktı vermedi ve 0 ile çıktı, yani syntax, expression ve action input'ları açısından hata bulmadı.

Mağazaya yüklemenin son adımını bu workflow'a koymadık. Yaygın seçenek fastlane: Android'de supply ile Play Console API'sine, iOS'ta pilot ile TestFlight'a yükleme, imzalama sertifikalarını ekipte paylaşmak için match. Alternatifler Play Developer API'sini kullanan hazır action'lar ve Xcode Cloud. Hepsinin ortak noktası, mağaza hesabına erişen bir servis hesabı ya da API anahtarı; onlar da secret olarak saklanıyor.

Üretimde Dikkat

Anahtarları yedekle. Upload keystore'u ve şifresini bir parola yöneticisinde ya da şirketin secret deposunda tut; yalnız bir geliştiricinin bilgisayarında duran bir anahtar, o bilgisayarla birlikte gider.

Sembol dosyalarını sürümle sakla. Workflow her release'in build/symbols klasörünü bundle'la birlikte artifact olarak saklıyor; crash raporlama servisine de yüklenmeli. Sembolsüz bir obfuscate stack trace okunamaz.

Kademeli yayın. Play'de staged rollout ile güncellemeyi önce kullanıcıların küçük bir yüzdesine, App Store'da phased release ile birkaç güne yayarak açarsın. Bir hata görürsen yayını durdurursun; mobilde geri alma yok, düzeltme ancak yeni bir sürümle gider.

Mağaza kuralları değişiyor. Target API, Xcode sürümü, gizlilik bildirimleri her yıl güncelleniyor; yayın öncesi kontrol listesine Play Console'un ve Apple'ın "upcoming requirements" sayfasına bakmayı ekle.

Sık Düşülen Tuzaklar

Birinci tuzak, key.properties'i ya da .jks'i commit'lemek. .gitignore, bir dosyanın git add -f ile eklenmesini engellemez; secret taraması CI'da da çalışmalı.

İkinci tuzak, aynı build numarasıyla ikinci kez yüklemek. İki mağaza da reddeder; numarayı CI'dan üret.

Üçüncü tuzak, --dart-define değerlerini secret sanmak. "Önce Tahmin Et"teki grep bunu bir saniyede gösteriyor.

Dördüncü tuzak, test kullanıcısı ve gizlilik metinlerini yayından bir gün önce düşünmek. İnceleme reddedilince her tur günler sürebilir.

Küçük Bir Deneme

★☆☆ config/dev.json'a "SHOW_DEBUG_TOOLS": true ekle, bool.fromEnvironment ile oku ve yalnız dev'de görünen bir menü öğesi koy. Prod build'inde değerin false olduğunu testle göster.

★★☆ Android'e staging ve production flavor'larını ekle (applicationIdSuffix = ".staging") ve appFlavor ile config dosyasını eşleştir. iOS tarafı için Flutter'ın ayrı flavor rehberini oku; hangi adımlar Xcode gerektiriyor?

★★★ Workflow'a iOS job'ı ekle: macOS runner, flutter build ipa, imzalama için App Store Connect API anahtarı ve TestFlight'a yükleme. Secret'ları hangi environment'a koyarsın, sertifikaların süresi dolunca ne olur?

Serinin son yazısında geriye bakıyoruz: Flutter 3 ve Dart 3'te neler değişti, bu seride kullandığımız özellikler hangi sürümde geldi, Flutter 3.50 ve material_ui ile neyin değiştiği ve bir Flutter projesi nasıl güncel tutulur.