İçeriğe geç
Masadan notlar

flutterkotlinspring bootapi

Flutter'ı Spring Boot (Kotlin) API'ye bağlamak: önce API sözleşmesi

Flutter modeli foundAt bekliyor, API found_at dönüyor ve ekran sessizce boş kalıyor. Flutter uygulamasını kendi Spring Boot (Kotlin) API'ne bağlarken önce sözleşmeyi yazmanın neden işe yaradığını ve nasıl yapılacağını örneklerle anlatıyoruz.

Can Karaca · 8 dk okuma

Mobil uygulamada ekran boş geliyor. Hata yok, log'da da dikkat çeken bir şey yok. Bir saat sonra sebep bulunuyor: Flutter modeli foundAt alanını bekliyor, API ise found_at dönüyor. İki taraf da "doğru" kodu yazmış; sadece aynı dili konuşmamışlar.

Flutter uygulamasını kendi yazdığın Spring Boot API'sine bağlarken asıl iş HTTP isteğini atmak değil, o isteğin ve cevabın tam olarak neye benzeyeceğinde anlaşmak. Bu yazıda önce bu anlaşmayı, yani API sözleşmesini yazıyoruz, sonra iki tarafı ona göre kodluyoruz.

API sözleşmesi neleri kapsar?

Sözleşme, istemci ile sunucu arasındaki yazılı anlaşmadır. En azından şunları netleştirmeli:

  • Endpoint yolları ve HTTP metotları (GET /api/items/{id} gibi)
  • İstek ve cevaptaki alanlar, tipleri ve hangilerinin boş (null) olabileceği
  • Alan adlandırma biçimi: foundAt mı, found_at mı?
  • Tarih formatı ve saat dilimi
  • Hangi durumda hangi durum kodu dönüleceği (200, 201, 400, 404...)
  • Hata cevabının şekli
  • Kimlik doğrulamanın nasıl yapılacağı

Bunu düz bir Markdown dosyasında da yazabilirsin. Ama HTTP API'lerini tanımlamak için kullanılan OpenAPI standardını seçersen dokümantasyon üretmek, sahte (mock) sunucu kurmak gibi işlerde hazır araçlardan da yararlanırsın.

Neden önce sözleşme?

  • Paralel çalışma: Sözleşme hazırsa mobil taraf, API bitmeden sahte verilerle ekranı yazabilir. API tarafı da ekranı beklemez.
  • Tartışma erkene taşınır: "Bu alan boş gelebilir mi?" sorusu kod yazıldıktan sonra değil, kısa bir dosya üzerinde konuşulur.
  • Review kolaylaşır: Reviewer, koda bakmadan önce "bu endpoint sözleşmeye uyuyor mu?" diye kontrol edebilir.

Gerçek bir örnek: emanet-app

re:make'in eğitim projesi emanet-app bu yaklaşımla yürüyor. Bir kayıp eşya uygulaması; ürün olarak değil, öğrenmek için kurulmuş bir proje. Flutter mobil uygulama, Kotlin ve Spring Boot ile yazılmış API ve PostgreSQL aynı repoda duruyor; Docker Compose ile paketlenip Caddy arkasında bir Linux sunucuda çalışıyor.

Projenin kurallarından ikisi bu yazının konusuyla doğrudan ilgili:

  • API sözleşmesi önce yazılır ve üzerinde anlaşılır, sonra herkes paralel çalışır.
  • Herkes kendi özelliğini uçtan uca yapar: ekranı, endpoint'i ve tabloyu aynı kişi yazar.

İkinci kural sözleşmeyi daha da önemli kılıyor. Aynı kişi iki tarafı da yazsa bile, başkası o endpoint'i kullanacak ya da genişletecek. Sözleşme, kişinin aklındaki varsayımları herkesin görebileceği bir yere taşıyor. Ödeme, mesajlaşma, bildirim ve harita gibi özelliklerin bilerek kapsam dışında tutulması da sözleşmeyi küçük ve yönetilebilir tutuyor.

Aşağıdaki kod örnekleri projenin gerçek dosyaları değil; benzer bir senaryo için yazılmış, basitleştirilmiş örnekler.

Adım 1: Sözleşmeyi yaz

Bulunan bir eşyanın detayını getiren endpoint için bir OpenAPI sözleşmesinden kısaltılmış bir parça:

yaml
paths:
  /api/items/{id}:
    get:
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Eşya bulundu
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Item" }
        "404":
          description: Eşya yok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
components:
  schemas:
    Item:
      type: object
      required: [id, title, locationText, foundAt, status]
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        locationText: { type: string }
        foundAt: { type: string, format: date-time }
        status: { type: string, enum: [OPEN, CLAIMED, RETURNED] }
        photoUrl: { type: [string, "null"] }
    ApiError:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }

Bu kısa dosyada birkaç önemli karar verildi: Alan adları camelCase, id bir UUID, tarih ISO 8601 biçiminde metin (2026-10-11T09:30:00Z gibi), durum büyük harfli bir enum, photoUrl boş olabilir ve hata cevabı her zaman code ile message içeriyor.

Adım 2: Spring Boot (Kotlin) tarafı

start.spring.io'da dil olarak Kotlin'i seçtiğinde, Kotlin data class'larını JSON'a çevirmek için gereken Jackson Kotlin modülü ve Spring'in Kotlin eklentisi projeye kendiliğinden eklenir. Sözleşmeyi Kotlin'e çevirmek neredeyse birebir (importlar kısalık için çıkarıldı):

kotlin
enum class ItemStatus { OPEN, CLAIMED, RETURNED }

data class ItemResponse(
    val id: UUID,
    val title: String,
    val locationText: String,
    val foundAt: Instant,
    val status: ItemStatus,
    val photoUrl: String?,
)

data class ApiError(val code: String, val message: String)

class NotFoundException(val code: String, message: String) : RuntimeException(message)

@RestController
@RequestMapping("/api/items")
class ItemController(private val items: ItemService) {
    @GetMapping("/{id}")
    fun getItem(@PathVariable id: UUID): ItemResponse =
        items.findById(id) ?: throw NotFoundException("ITEM_NOT_FOUND", "Eşya bulunamadı")
}

@RestControllerAdvice
class ApiErrorHandler {
    @ExceptionHandler(NotFoundException::class)
    fun notFound(e: NotFoundException) =
        ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiError(e.code, e.message ?: ""))
}

Kotlin'deki String? ile sözleşmedeki "boş olabilir" bilgisi aynı şeyi söylüyor; bu eşleşmeyi review'da kontrol etmek kolay. Hataları @RestControllerAdvice ile tek yerde ele almak, her endpoint'in sözleşmedeki hata şeklini aynı biçimde dönmesini sağlar. Tarih alanının gerçekten ISO 8601 metni olarak çıktığını da bir testle doğrula; sözleşmeden sapmalar en çok tarih ve enum alanlarında olur.

Sözleşme ile kodun birbirinden uzaklaşmadığını kontrol etmek için springdoc-openapi kütüphanesini ekleyebilirsin. Kodundan ürettiği OpenAPI dokümanını varsayılan olarak /v3/api-docs, Swagger arayüzünü de /swagger-ui.html adresinde sunar. Kullandığın Spring Boot sürümüne uygun springdoc sürümünü seçmeyi unutma.

Adım 3: Flutter tarafı

Flutter'da HTTP istekleri için resmi dokümanların da kullandığı http paketini ekle:

bash
flutter pub add http

Model sınıfı, sözleşmedeki şemanın Dart karşılığı:

dart
enum ItemStatus { open, claimed, returned }

class Item {
  final String id;
  final String title;
  final String locationText;
  final DateTime foundAt;
  final ItemStatus status;
  final String? photoUrl;

  const Item({required this.id, required this.title, required this.locationText,
      required this.foundAt, required this.status, this.photoUrl});

  factory Item.fromJson(Map<String, dynamic> json) => Item(
        id: json['id'] as String,
        title: json['title'] as String,
        locationText: json['locationText'] as String,
        foundAt: DateTime.parse(json['foundAt'] as String),
        status: ItemStatus.values.byName((json['status'] as String).toLowerCase()),
        photoUrl: json['photoUrl'] as String?,
      );
}

API adresini koda gömmek yerine derleme sırasında ver:

dart
import 'dart:convert';
import 'package:http/http.dart' as http;

const apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'http://10.0.2.2:8080');

class ApiException implements Exception {
  final int statusCode;
  final String body;
  ApiException(this.statusCode, this.body);
}

Future<Item> fetchItem(String id) async {
  final response = await http.get(
    Uri.parse('$apiBaseUrl/api/items/$id'),
    headers: {'Accept': 'application/json'},
  );
  if (response.statusCode == 200) {
    return Item.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  }
  throw ApiException(response.statusCode, response.body);
}

Uygulamayı çalıştırırken adresi şöyle verirsin:

bash
flutter run --dart-define=API_BASE_URL=https://api.ornek.com

Hata cevabını sözleşmedeki code alanına göre ayrıştırırsan kullanıcıya "Bir hata oluştu" yerine "Bu eşya artık listede değil" gibi anlamlı bir mesaj gösterebilirsin.

Bağlantıda en sık takılınan yerler

  • Android emülatöründe localhost çalışmaz. Emülatörün içindeki 127.0.0.1 emülatörün kendisidir. Bilgisayarındaki API'ye ulaşmak için 10.0.2.2 adresini kullan. iOS simülatörü ise bilgisayarın ağını paylaştığı için localhost ile ulaşabilir.
  • Gerçek cihazda bilgisayarın yerel ağ IP'sini kullan. Telefon ile bilgisayar aynı ağda olmalı. API Docker içinde çalışıyorsa portun dışarı açık olduğundan emin ol; bilgisayarının güvenlik duvarı da isteği engelleyebilir.
  • Debug'da çalışıyor, release'de çalışmıyor. Android'de internet izni ana manifest dosyasında yoksa release derlemede ağ istekleri başarısız olur. android/app/src/main/AndroidManifest.xml dosyasına şu satırı ekle:
html/xml
<uses-permission android:name="android.permission.INTERNET" />
  • Mobilde CORS hatası olmaz. CORS tarayıcıların uyguladığı bir mekanizma; Android ya da iOS uygulamasından giden istek buna takılmaz. Aynı uygulamayı Flutter web olarak da çalıştıracaksan API tarafında CORS ayarı gerekir.
  • Canlıda HTTPS kullan. Şifresiz HTTP'yi yalnızca yerel geliştirmede kullan. emanet-app'te API'nin önünde duran Caddy, TLS sertifikalarını otomatik alıp yenileyebilen bir web sunucusu; HTTPS için ayrıca uğraşmak gerekmiyor.

Sözleşme değiştiğinde

Sözleşme bir kez yazılıp unutulan bir belge değil, ama değişikliği kontrollü olmalı:

  1. Önce sözleşme dosyası bir pull request ile değişir ve tartışılır.
  2. Yeni alan eklemek genelde güvenlidir; yukarıdaki gibi yazılmış bir fromJson tanımadığı alanı görmezden gelir.
  3. Alan silmek ya da yeniden adlandırmak kırıcı bir değişikliktir. Önce yeni alanı ekle, istemciyi güncelle, sonra eskisini kaldır.
  4. Testler her PR'da otomatik çalışsın. emanet-app'te GitHub Actions her pull request'te testleri çalıştırıyor ve main'e merge edilen kod sunucuya otomatik deploy ediliyor. Sözleşmeyi bozan bir değişikliğin canlıya gitmeden yakalanması bu yüzden önemli.

Son söz

Sözleşmeyi önce yazmak ilk gün yavaşlatıyormuş gibi gelir. Ama mobil ve API kodunun aynı dili konuştuğu her gün, sonradan hata ayıklamaya harcanacak saatlerin önüne geçer. emanet-app'in kapsamını, kurallarını ve teknoloji yığınını proje sayfasında görebilirsin.

XLinkedIn

Bir sonraki not düştüğünde haberin olsun.

Yeni yazılar ve masadan notlar. Ayda birkaç mail, istediğin an çık.

Masada konuşulanlar 0

Yorum yaz

Masadan başka notlar

Okumaya devam.

  1. 11 Ekim 2026

    TÜBİTAK 2209-A 2026 açıldı: yazılım projesiyle nasıl başvurulur?

    TÜBİTAK'ın lisans ve ön lisans öğrencilerine yönelik 2209-A ve 2209-B çağrıları 11 Kasım 2026'ya kadar açık. Yazılım fikrini hakemin görmek istediği araştırma projesine nasıl çevireceğini ve önümüzdeki dört haftayı nasıl planlayacağını anlattık.

    TÜBİTAK · 2209-A · öğrenci · araştırma projesi · kariyer

    Can Karaca · 5 dk okuma
  2. 11 Ekim 2026

    Hacktoberfest 2026'da PR sayılmıyor: yapay zekâ çağında açık kaynak

    Dört PR'a tişört dönemi bitti: Hacktoberfest 2026'da pull request'ler ödüle sayılmıyor, GitHub da bakımcılara yeni PR sınırları veriyor. Bu değişikliğin junior geliştirici için anlamını ve yapay zekâ yardımıyla bile güven veren bir katkının nasıl yapılacağını anlattık.

    açık kaynak · Hacktoberfest · etkinlik · github · yapay zekâ

    Can Karaca · 5 dk okuma
  3. 11 Ekim 2026

    Python 3.15 çıktı: öğrenciler için önemli yenilikler ve geçiş rehberi

    Python 3.15 yayımlandı ve Türkçe karakterle çalışan herkesi ilgilendiren bir varsayılan değişti: UTF-8 artık her yerde varsayılan. Öğrenci gözüyle en önemli yenilikleri, Python 3.10'un destek sonunu ve bu ay yapabileceğin beş somut adımı anlattık.

    öğrenci · Python · Python 3.15 · programlama dilleri

    Can Karaca · 5 dk okuma